diff --git a/AGENTS.md b/AGENTS.md index 251c55d..d1c90d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,8 +19,21 @@ All project documentation lives in `doc/`. Load only what you need. | Parse/modify `.pro` song files | `doc/api/song.md` | | Parse/modify `.proplaylist` files | `doc/api/playlist.md` | | Parse/modify `.probundle` files | `doc/api/bundle.md` | -| Read the global `Macros` file | `doc/api/macros.md` | -| Read the global `Labels` file | `doc/api/labels.md` | +| Read/write the global `Macros` file | `doc/api/macros.md` | +| Read/write the global `Labels` file | `doc/api/labels.md` | +| Read/write the global `Groups` file | `doc/api/groups.md` | +| Read/write the global `ClearGroups` file | `doc/api/clear-groups.md` | +| Read/write the global `CCLI` file | `doc/api/ccli.md` | +| Read/write the global `Messages` file | `doc/api/messages.md` | +| Read/write the global `Timers` file | `doc/api/timers.md` | +| Read/write the global `Stage` file | `doc/api/stage.md` | +| Read/write the global `Workspace` file | `doc/api/workspace.md` | +| Read/write the global `Props` file | `doc/api/props.md` | +| Read/write the global `TestPatterns` file | `doc/api/test-patterns.md` | +| Read/write the global `Calendar` file | `doc/api/calendar.md` | +| Read/write the global `KeyMappings` file | `doc/api/key-mappings.md` | +| Read/write the `CommunicationDevices` JSON file | `doc/api/communication-devices.md` | +| Read/write a theme folder (Theme + Assets/) | `doc/api/theme.md` | | Understand `.pro` binary format | `doc/formats/pp_song_spec.md` | | Understand `.proplaylist` binary format | `doc/formats/pp_playlist_spec.md` | | Understand `.probundle` binary format | `doc/formats/pp_bundle_spec.md` | @@ -57,16 +70,34 @@ PHP tools for parsing, modifying, and generating ProPresenter 7 files: - **Songs** (`.pro`) — Protobuf-encoded presentation files with lyrics, groups, slides, arrangements, translations - **Playlists** (`.proplaylist`) — ZIP64 archives containing playlist metadata and embedded songs - **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets -- **Macros** (`Macros`, no extension) — Global protobuf-encoded macro library with collections -- **Labels** (`Labels`, no extension) — Global protobuf-encoded label library (slide labels with optional UI colors) +- **Themes** (folder with `Theme` + `Assets/`) — Template document plus media used as a slide theme +- **Global library files** (no extension) — `Macros`, `Labels`, `Groups`, `ClearGroups`, `CCLI`, `Messages`, `Timers`, `Stage`, `Workspace`, `Props`, `TestPatterns`, `Calendar`, `KeyMappings` (protobuf) and `CommunicationDevices` (JSON) ### CLI Tools ```bash +# Songs / playlists / bundles php bin/parse-song.php path/to/song.pro php bin/parse-playlist.php path/to/playlist.proplaylist + +# Global library files (one parser per type) php bin/parse-macros.php path/to/Macros php bin/parse-labels.php path/to/Labels +php bin/parse-groups.php path/to/Groups +php bin/parse-clear-groups.php path/to/ClearGroups +php bin/parse-ccli.php path/to/CCLI +php bin/parse-messages.php path/to/Messages +php bin/parse-timers.php path/to/Timers +php bin/parse-stage.php path/to/Stage +php bin/parse-workspace.php path/to/Workspace +php bin/parse-props.php path/to/Props +php bin/parse-test-patterns.php path/to/TestPatterns +php bin/parse-calendar.php path/to/Calendar +php bin/parse-key-mappings.php path/to/KeyMappings +php bin/parse-communication-devices.php path/to/CommunicationDevices + +# Theme folder +php bin/parse-theme.php path/to/ThemeFolder ``` ### Key Source Files diff --git a/bin/parse-calendar.php b/bin/parse-calendar.php new file mode 100755 index 0000000..cb6723a --- /dev/null +++ b/bin/parse-calendar.php @@ -0,0 +1,26 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +try { + $library = CalendarFileReader::read($argv[1]); +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . "\n"; + exit(1); +} + +$events = $library->getEvents(); +echo 'Calendar events (' . count($events) . ') mode=' . $library->getMode() . ":\n"; +foreach ($events as $index => $event) { + echo sprintf(" [%d] %s :: %s :: start=%s :: end=%s :: action=%dB :: macro=%dB\n", $index + 1, $event->getName() === '' ? '(unnamed)' : $event->getName(), $event->getUuid(), (string) ($event->getStartTimeSeconds() ?? ''), (string) ($event->getEndTimeSeconds() ?? ''), strlen($event->getActionData()), strlen($event->getMacroData())); +} diff --git a/bin/parse-ccli.php b/bin/parse-ccli.php new file mode 100644 index 0000000..a7eb506 --- /dev/null +++ b/bin/parse-ccli.php @@ -0,0 +1,25 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +$filePath = $argv[1]; + +try { + $library = CCLIFileReader::read($filePath); +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + exit(1); +} + +echo "CCLI (1):\n"; +echo sprintf(" [1] enabled=%s :: license=%s :: display_type=%d :: template=%s\n", $library->isCCLIDisplayEnabled() ? 'yes' : 'no', $library->getCCLILicense() === '' ? '(empty)' : $library->getCCLILicense(), $library->getDisplayType(), $library->getTemplate() === null ? 'no' : 'yes'); diff --git a/bin/parse-clear-groups.php b/bin/parse-clear-groups.php new file mode 100644 index 0000000..06d27c7 --- /dev/null +++ b/bin/parse-clear-groups.php @@ -0,0 +1,35 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +$filePath = $argv[1]; + +try { + $library = ClearGroupsFileReader::read($filePath); +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + exit(1); +} + +$groups = $library->getGroups(); + +echo "ClearGroups (" . count($groups) . "):\n"; +foreach ($groups as $index => $group) { + $number = $index + 1; + $name = $group->getName(); + $displayName = $name === '' ? '(unnamed)' : $name; + $uuid = $group->getUuid(); + $colorPart = $group->getColorHex() ?? '(no tint)'; + + echo sprintf(" [%d] %s :: %s :: image_type=%d :: %s\n", $number, $displayName, $uuid, $group->getImageType(), $colorPart); +} diff --git a/bin/parse-communication-devices.php b/bin/parse-communication-devices.php new file mode 100755 index 0000000..2344e71 --- /dev/null +++ b/bin/parse-communication-devices.php @@ -0,0 +1,29 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +try { + $library = CommunicationDevicesFileReader::read($argv[1]); +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . "\n"; + exit(1); +} + +$devices = $library->getDevices(); +echo 'Communication devices (' . count($devices) . "):\n"; +if ($devices === []) { + echo " (none configured)\n"; +} +foreach ($devices as $index => $device) { + echo sprintf(" [%d] %s :: %s :: %s :: %s\n", $index + 1, $device->getName() === '' ? '(unnamed)' : $device->getName(), $device->getId(), $device->getType(), $device->getAddress()); +} diff --git a/bin/parse-groups.php b/bin/parse-groups.php new file mode 100755 index 0000000..9915c58 --- /dev/null +++ b/bin/parse-groups.php @@ -0,0 +1,35 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +$filePath = $argv[1]; + +try { + $library = GroupsFileReader::read($filePath); +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + exit(1); +} + +$groups = $library->getGroups(); + +echo "Groups (" . count($groups) . "):\n"; +foreach ($groups as $index => $group) { + $number = $index + 1; + $name = $group->getName(); + $displayName = $name === '' ? '(unnamed)' : $name; + $uuid = $group->getUuid(); + $colorPart = $group->getColorHex() ?? '(no color)'; + + echo sprintf(" [%d] %s :: %s :: %s\n", $number, $displayName, $uuid, $colorPart); +} diff --git a/bin/parse-key-mappings.php b/bin/parse-key-mappings.php new file mode 100644 index 0000000..14d8f4c --- /dev/null +++ b/bin/parse-key-mappings.php @@ -0,0 +1,35 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +$filePath = $argv[1]; + +try { + $library = KeyMappingsFileReader::read($filePath); +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + exit(1); +} + +$mappings = $library->getMappings(); + +echo "KeyMappings (" . count($mappings) . "):\n"; +foreach ($mappings as $index => $mapping) { + $number = $index + 1; + $name = $mapping->getName(); + $displayName = $name === '' ? '(unnamed)' : $name; + $uuid = $mapping->getUuid(); + $hotKeyPart = $mapping->getHotKey() === null ? '(no hot key)' : 'hot_key=yes'; + + echo sprintf(" [%d] %s :: %s :: target_bytes=%d :: %s\n", $number, $displayName, $uuid, strlen($mapping->getTarget()), $hotKeyPart); +} diff --git a/bin/parse-messages.php b/bin/parse-messages.php new file mode 100755 index 0000000..cf108c4 --- /dev/null +++ b/bin/parse-messages.php @@ -0,0 +1,27 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +try { + $library = MessagesFileReader::read($argv[1]); +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . "\n"; + exit(1); +} + +$messages = $library->getMessages(); +echo 'Messages (' . count($messages) . "):\n"; +foreach ($messages as $index => $message) { + $title = $message->getTitle() === '' ? '(untitled)' : $message->getTitle(); + echo sprintf(" [%d] %s :: %s :: clear=%d :: network=%s\n", $index + 1, $title, $message->getUuid(), $message->getClearType(), $message->isVisibleOnNetwork() ? 'yes' : 'no'); +} diff --git a/bin/parse-props.php b/bin/parse-props.php new file mode 100644 index 0000000..387c98f --- /dev/null +++ b/bin/parse-props.php @@ -0,0 +1,25 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +try { + $library = PropsFileReader::read($argv[1]); +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . "\n"; + exit(1); +} + +echo 'Props (' . $library->count() . "):\n"; +foreach ($library->getProps() as $index => $prop) { + echo sprintf(" [%d] %s :: %s :: %s\n", $index + 1, $prop->getName() ?: '(unnamed)', $prop->getUuid(), $prop->isEnabled() ? 'enabled' : 'disabled'); +} diff --git a/bin/parse-stage.php b/bin/parse-stage.php new file mode 100644 index 0000000..0d14371 --- /dev/null +++ b/bin/parse-stage.php @@ -0,0 +1,25 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +try { + $library = StageFileReader::read($argv[1]); +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . "\n"; + exit(1); +} + +echo 'Stage layouts (' . $library->count() . "):\n"; +foreach ($library->getLayouts() as $index => $layout) { + echo sprintf(" [%d] %s :: %s\n", $index + 1, $layout->getName() ?: '(unnamed)', $layout->getUuid()); +} diff --git a/bin/parse-test-patterns.php b/bin/parse-test-patterns.php new file mode 100644 index 0000000..4ba235e --- /dev/null +++ b/bin/parse-test-patterns.php @@ -0,0 +1,34 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +$filePath = $argv[1]; + +try { + $library = TestPatternsFileReader::read($filePath); +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + exit(1); +} + +echo "TestPatterns (" . $library->count() . "):\n"; +echo sprintf(" State: selected=%s :: name=%s :: display_location=%d :: screen=%s\n", $library->getSelectedPatternUuid() === '' ? '(none)' : $library->getSelectedPatternUuid(), $library->getSelectedPatternNameLocalizationKey() === '' ? '(none)' : $library->getSelectedPatternNameLocalizationKey(), $library->getDisplayLocation(), $library->getSpecificScreenUuid() === '' ? '(none)' : $library->getSpecificScreenUuid()); + +foreach ($library->getPatterns() as $index => $pattern) { + $number = $index + 1; + $name = $pattern->getNameLocalizationKey(); + $displayName = $name === '' ? '(unnamed)' : $name; + $uuid = $pattern->getUuid()?->getString() ?? ''; + + echo sprintf(" [%d] %s :: %s\n", $number, $displayName, $uuid); +} diff --git a/bin/parse-theme.php b/bin/parse-theme.php new file mode 100644 index 0000000..c01f430 --- /dev/null +++ b/bin/parse-theme.php @@ -0,0 +1,30 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +try { + $bundle = ThemeFileReader::read($argv[1]); +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . "\n"; + exit(1); +} + +echo "Theme folder: {$argv[1]}\n"; +echo 'Slides (' . $bundle->count() . "):\n"; +foreach ($bundle->getSlides() as $index => $slide) { + echo sprintf(" [%d] %s\n", $index + 1, $slide->getName() ?: '(unnamed)'); +} +echo 'Assets (' . $bundle->getAssetCount() . "):\n"; +foreach ($bundle->getAssets() as $index => $asset) { + echo sprintf(" [%d] Assets/%s :: %d bytes :: %s\n", $index + 1, $asset->getName(), $asset->getSize(), $asset->getMimeType()); +} diff --git a/bin/parse-timers.php b/bin/parse-timers.php new file mode 100755 index 0000000..a54a8d0 --- /dev/null +++ b/bin/parse-timers.php @@ -0,0 +1,29 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +try { + $library = TimersFileReader::read($argv[1]); +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . "\n"; + exit(1); +} + +$timers = $library->getTimers(); +echo 'Timers (' . count($timers) . ') clock=' . $library->getClockFormat() . ":\n"; +foreach ($timers as $index => $timer) { + $type = $timer->isCountdown() ? 'countdown' : ($timer->isCountdownToTime() ? 'countdown_to_time' : ($timer->isElapsedTime() ? 'elapsed_time' : 'unknown')); + $duration = $timer->getDurationSeconds(); + $durationPart = $duration === null ? '' : sprintf(' :: %ds', $duration); + echo sprintf(" [%d] %s :: %s :: %s%s\n", $index + 1, $timer->getName() === '' ? '(unnamed)' : $timer->getName(), $timer->getUuid(), $type, $durationPart); +} diff --git a/bin/parse-workspace.php b/bin/parse-workspace.php new file mode 100644 index 0000000..68ff857 --- /dev/null +++ b/bin/parse-workspace.php @@ -0,0 +1,25 @@ +#!/usr/bin/env php +\n"; + exit(1); +} + +try { + $library = WorkspaceFileReader::read($argv[1]); +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . "\n"; + exit(1); +} + +echo 'Screens (' . $library->count() . "):\n"; +foreach ($library->getScreens() as $index => $screen) { + echo sprintf(" [%d] %s :: %s :: type %d\n", $index + 1, $screen->getName() ?: '(unnamed)', $screen->getUuid(), $screen->getScreenType()); +} diff --git a/doc/INDEX.md b/doc/INDEX.md index 30a6323..153a221 100644 --- a/doc/INDEX.md +++ b/doc/INDEX.md @@ -9,8 +9,21 @@ | Parse/modify `.pro` song files | [api/song.md](api/song.md) | | Parse/modify `.proplaylist` files | [api/playlist.md](api/playlist.md) | | Parse/modify `.probundle` files | [api/bundle.md](api/bundle.md) | -| Read the global `Macros` file | [api/macros.md](api/macros.md) | -| Read the global `Labels` file | [api/labels.md](api/labels.md) | +| Read/write the global `Macros` file | [api/macros.md](api/macros.md) | +| Read/write the global `Labels` file | [api/labels.md](api/labels.md) | +| Read/write the global `Groups` file | [api/groups.md](api/groups.md) | +| Read/write the global `ClearGroups` file | [api/clear-groups.md](api/clear-groups.md) | +| Read/write the global `CCLI` file | [api/ccli.md](api/ccli.md) | +| Read/write the global `Messages` file | [api/messages.md](api/messages.md) | +| Read/write the global `Timers` file | [api/timers.md](api/timers.md) | +| Read/write the global `Stage` file | [api/stage.md](api/stage.md) | +| Read/write the global `Workspace` file | [api/workspace.md](api/workspace.md) | +| Read/write the global `Props` file | [api/props.md](api/props.md) | +| Read/write the global `TestPatterns` file | [api/test-patterns.md](api/test-patterns.md) | +| Read/write the global `Calendar` file | [api/calendar.md](api/calendar.md) | +| Read/write the global `KeyMappings` file | [api/key-mappings.md](api/key-mappings.md) | +| Read/write the global `CommunicationDevices` JSON file | [api/communication-devices.md](api/communication-devices.md) | +| Read/write theme folders (Theme + Assets/) | [api/theme.md](api/theme.md) | | Understand `.pro` binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md) | | Understand `.proplaylist` format | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | | Understand `.probundle` format | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | @@ -27,11 +40,27 @@ - [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) — ProPresenter 7 `.probundle` file format (ZIP container, media assets) ### PHP API Documentation +#### Document containers - [api/song.md](api/song.md) — Song parser API (read, modify, generate `.pro` files) - [api/playlist.md](api/playlist.md) — Playlist parser API (read, modify, generate `.proplaylist` files) - [api/bundle.md](api/bundle.md) — Bundle parser API (read, write `.probundle` files with media) -- [api/macros.md](api/macros.md) — Macros library API (read the global `Macros` file) -- [api/labels.md](api/labels.md) — Labels library API (read the global `Labels` file) +- [api/theme.md](api/theme.md) — Theme bundle API (folder with `Theme` proto + `Assets/`) + +#### Global library files (read + write) +- [api/macros.md](api/macros.md) — `Macros` library (macros + collections, with editable accessors) +- [api/labels.md](api/labels.md) — `Labels` library (named slide labels with optional UI colors) +- [api/groups.md](api/groups.md) — `Groups` library (named library groups with UUID, color, hot keys) +- [api/clear-groups.md](api/clear-groups.md) — `ClearGroups` library (clear-action groups) +- [api/ccli.md](api/ccli.md) — `CCLI` settings (license, display behaviour, copyright template) +- [api/messages.md](api/messages.md) — `Messages` library (lower-third / overlay messages) +- [api/timers.md](api/timers.md) — `Timers` library (timer definitions + clock format) +- [api/stage.md](api/stage.md) — `Stage` document (stage display layouts) +- [api/workspace.md](api/workspace.md) — `Workspace` document (screens, looks, masks, audio/video inputs) +- [api/props.md](api/props.md) — `Props` document (prop cues + transition) +- [api/test-patterns.md](api/test-patterns.md) — `TestPatterns` document (currently selected pattern + saved overrides) +- [api/calendar.md](api/calendar.md) — `Calendar` document (scheduled events firing macros) +- [api/key-mappings.md](api/key-mappings.md) — `KeyMappings` document (custom hot-key bindings) +- [api/communication-devices.md](api/communication-devices.md) — `CommunicationDevices` JSON list (MIDI / serial / OSC bindings) ### Internal Reference - [internal/learnings.md](internal/learnings.md) — Development learnings and conventions discovered @@ -59,8 +88,21 @@ doc/ │ ├── song.md │ ├── playlist.md │ ├── bundle.md +│ ├── theme.md │ ├── macros.md -│ └── labels.md +│ ├── labels.md +│ ├── groups.md +│ ├── clear-groups.md +│ ├── ccli.md +│ ├── messages.md +│ ├── timers.md +│ ├── stage.md +│ ├── workspace.md +│ ├── props.md +│ ├── test-patterns.md +│ ├── calendar.md +│ ├── key-mappings.md +│ └── communication-devices.md └── internal/ ← Development notes (optional context) ├── learnings.md ├── decisions.md @@ -86,6 +128,17 @@ Load: doc/api/playlist.md Load: doc/api/bundle.md ``` +### Task: "Edit a global library file (Macros, Labels, Groups, etc.)" +``` +Load: doc/api/.md +``` + +### Task: "Round-trip a Theme folder with assets" +``` +Load: doc/api/theme.md +Load: doc/api/bundle.md (for the bundle pattern reference) +``` + ### Task: "Debug protobuf parsing issues" ``` Load: doc/formats/pp_song_spec.md (sections 2-5) @@ -113,6 +166,8 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi - **Songs** (`.pro`) — Presentation files containing lyrics with groups, slides, arrangements, and translations - **Playlists** (`.proplaylist`) — ZIP archives containing playlist metadata and embedded song files - **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets +- **Global library files** — `Macros`, `Labels`, `Groups`, `ClearGroups`, `CCLI`, `Messages`, `Timers`, `Stage`, `Workspace`, `Props`, `TestPatterns`, `Calendar`, `KeyMappings`, `CommunicationDevices` (JSON) +- **Theme folders** — directory with a `Theme` protobuf file plus an `Assets/` subdirectory of media ### Key Components @@ -129,23 +184,46 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi | `src/PresentationBundle.php` | Bundle wrapper (read/write `.probundle` files) | | `src/ProBundleReader.php` | Read `.probundle` files | | `src/ProBundleWriter.php` | Write `.probundle` files | -| `src/MacroLibrary.php` | Macros library wrapper (read global `Macros` file) | -| `src/MacrosFileReader.php` | Read global `Macros` file | -| `src/LabelLibrary.php` | Labels library wrapper (read global `Labels` file) | -| `src/LabelsFileReader.php` | Read global `Labels` file | +| `src/ThemeBundle.php` | Theme folder wrapper (Template.Document + Assets/) | +| `src/ThemeFileReader.php` / `ThemeFileWriter.php` | Read/write theme folders | +| `src/MacroLibrary.php` / `MacrosFileReader.php` / `MacrosFileWriter.php` | Macros file IO | +| `src/LabelLibrary.php` / `LabelsFileReader.php` / `LabelsFileWriter.php` | Labels file IO | +| `src/GroupLibrary.php` / `GroupsFileReader.php` / `GroupsFileWriter.php` | Groups file IO | +| `src/ClearGroupsLibrary.php` / `ClearGroupsFileReader.php` / `ClearGroupsFileWriter.php` | ClearGroups file IO | +| `src/CCLILibrary.php` / `CCLIFileReader.php` / `CCLIFileWriter.php` | CCLI file IO | +| `src/MessageLibrary.php` / `MessagesFileReader.php` / `MessagesFileWriter.php` | Messages file IO | +| `src/TimersLibrary.php` / `TimersFileReader.php` / `TimersFileWriter.php` | Timers file IO | +| `src/StageLibrary.php` / `StageFileReader.php` / `StageFileWriter.php` | Stage file IO | +| `src/WorkspaceLibrary.php` / `WorkspaceFileReader.php` / `WorkspaceFileWriter.php` | Workspace file IO | +| `src/PropLibrary.php` / `PropsFileReader.php` / `PropsFileWriter.php` | Props file IO | +| `src/TestPatternsLibrary.php` / `TestPatternsFileReader.php` / `TestPatternsFileWriter.php` | TestPatterns file IO | +| `src/CalendarLibrary.php` / `CalendarFileReader.php` / `CalendarFileWriter.php` | Calendar file IO | +| `src/KeyMappingsLibrary.php` / `KeyMappingsFileReader.php` / `KeyMappingsFileWriter.php` | KeyMappings file IO | +| `src/CommunicationDevicesLibrary.php` / `CommunicationDevicesFileReader.php` / `CommunicationDevicesFileWriter.php` | CommunicationDevices JSON file IO | ### CLI Tools ```bash -# Parse and display song structure +# Songs / playlists / bundles php bin/parse-song.php path/to/song.pro - -# Parse and display playlist structure php bin/parse-playlist.php path/to/playlist.proplaylist -# Parse and display the global Macros file +# Global library files php bin/parse-macros.php path/to/Macros - -# Parse and display the global Labels file php bin/parse-labels.php path/to/Labels +php bin/parse-groups.php path/to/Groups +php bin/parse-clear-groups.php path/to/ClearGroups +php bin/parse-ccli.php path/to/CCLI +php bin/parse-messages.php path/to/Messages +php bin/parse-timers.php path/to/Timers +php bin/parse-stage.php path/to/Stage +php bin/parse-workspace.php path/to/Workspace +php bin/parse-props.php path/to/Props +php bin/parse-test-patterns.php path/to/TestPatterns +php bin/parse-calendar.php path/to/Calendar +php bin/parse-key-mappings.php path/to/KeyMappings +php bin/parse-communication-devices.php path/to/CommunicationDevices + +# Theme folder +php bin/parse-theme.php path/to/ThemeFolder ``` diff --git a/doc/api/calendar.md b/doc/api/calendar.md new file mode 100644 index 0000000..8335012 --- /dev/null +++ b/doc/api/calendar.md @@ -0,0 +1,115 @@ +# Calendar Library API + +> PHP module for reading and writing the global ProPresenter `Calendar` file +> (raw protobuf, no extension) and preserving scheduled macro events. + +## Quick Reference + +```php +use ProPresenter\Parser\CalendarFileReader; + +$library = CalendarFileReader::read('/path/to/Calendar'); + +foreach ($library->getEvents() as $event) { + $event->getName(); + $event->getStartTimeSeconds(); + $event->getActionData(); // raw bytes +} +``` + +--- + +## File Layout + +The `Calendar` file is the protobuf-serialised +[`CalendarDocument`](../../proto/calendar.proto): + +| Field | Type | Description | +|-------|------|-------------| +| `events` | repeated `CalendarDocument.Event` | Scheduled events | +| `mode` | `uint32` | Opaque document mode/source flag | + +Each event includes UUID, name, start/end timestamps, opaque `flags`, and two +raw bytes fields: `action_data` (field 8) and `macro_data` (field 9). Those +bytes are intentionally not decoded by this wrapper. + +--- + +## Reading + +```php +$library = CalendarFileReader::read('/Users/me/.../Calendar'); +``` + +Throws `InvalidArgumentException` for missing files and `RuntimeException` for +empty / unreadable files. + +--- + +## CalendarLibrary + +```php +$library->getEvents(); // CalendarEvent[] +$library->count(); // int +$library->getEventByUuid($uuid); // ?CalendarEvent (case-insensitive) +$library->getEventByName($name); // ?CalendarEvent +$library->addEvent($name, $uuid); // CalendarEvent +$library->removeEvent($uuid); // bool +$library->getMode(); // int +$library->setMode(1); // void +$library->getDocument(); // \Rv\Data\CalendarDocument +``` + +--- + +## CalendarEvent + +```php +$event->getUuid(); +$event->setUuid($uuid); +$event->getName(); +$event->setName($name); +$event->getStartTime(); +$event->setStartTime($timestamp); +$event->getStartTimeSeconds(); +$event->setStartTimeSeconds($seconds); +$event->getEndTime(); +$event->getEndTimeSeconds(); +$event->getFlags(); +$event->setFlags($bytes); +$event->getActionData(); // raw protobuf bytes +$event->setActionData($bytes); +$event->getMacroData(); // raw protobuf bytes +$event->setMacroData($bytes); +$event->getProto(); +``` + +--- + +## CLI Tool + +```bash +php bin/parse-calendar.php /path/to/Calendar +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/CalendarLibrary.php` | Document wrapper with UUID / name indexes | +| `src/CalendarEvent.php` | Single event wrapper | +| `src/CalendarFileReader.php` | Reads the `Calendar` file | +| `src/CalendarFileWriter.php` | Writes the `Calendar` file | +| `bin/parse-calendar.php` | CLI summary tool | +| `proto/calendar.proto` | Protobuf schema | +| `generated/Rv/Data/CalendarDocument.php` | Generated document class | + +--- + +## Scope Notes + +`action_data` and `macro_data` are raw protobuf byte strings. They are exposed +directly for byte-preserving edits and round trips; semantic decoding belongs in +a future schema-specific module. diff --git a/doc/api/ccli.md b/doc/api/ccli.md new file mode 100644 index 0000000..7000e7d --- /dev/null +++ b/doc/api/ccli.md @@ -0,0 +1,104 @@ +# CCLI Library API + +> PHP module for reading and writing the global ProPresenter `CCLI` file (raw +> protobuf, no extension) that controls copyright display settings. + +## Quick Reference + +```php +use ProPresenter\Parser\CCLIFileReader; +use ProPresenter\Parser\CCLIFileWriter; + +$library = CCLIFileReader::read('/path/to/CCLI'); + +$library->isCCLIDisplayEnabled(); // bool +$library->getCCLILicense(); // string +$library->getDisplayType(); // int enum value +$library->getTemplate(); // ?\Rv\Data\Template\Slide + +$library->setCCLILicense('1234567'); +CCLIFileWriter::write($library, '/path/to/CCLI'); +``` + +--- + +## File Layout + +The `CCLI` file is the protobuf-serialised +[`CCLIDocument`](../../proto/ccli.proto): + +| Field | Type | Description | +|-------|------|-------------| +| `application_info` | `ApplicationInfo` | ProPresenter writer metadata | +| `enable_ccli_display` | bool | Whether copyright info is shown | +| `ccli_license` | string | CCLI license number | +| `display_type` | `CCLIDocument.DisplayType` | First, last, first+last, or all slides | +| `template` | `Template.Slide` | Text/template styling for display | + +--- + +## Reading + +```php +use ProPresenter\Parser\CCLIFileReader; + +$library = CCLIFileReader::read('/Users/me/.../CCLI'); +``` + +Throws `InvalidArgumentException` for missing files and `RuntimeException` for +empty / unreadable files. + +--- + +## CCLILibrary + +Top-level wrapper around `Rv\Data\CCLIDocument`. This is a single-document +configuration file; `count()` returns `1` when read successfully. + +```php +$library->count(); +$library->isCCLIDisplayEnabled(); +$library->setCCLIDisplayEnabled(true); +$library->getCCLILicense(); +$library->setCCLILicense('1234567'); +$library->getDisplayType(); +$library->setDisplayType(3); +$library->getTemplate(); +$library->setTemplate($slideOrNull); +$library->getDocument(); // \Rv\Data\CCLIDocument +``` + +--- + +## CLI Tool + +```bash +php bin/parse-ccli.php /path/to/CCLI +``` + +Output: + +``` +CCLI (1): + [1] enabled=yes :: license=(empty) :: display_type=0 :: template=yes +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/CCLILibrary.php` | Document-level wrapper | +| `src/CCLIFileReader.php` | Reads the `CCLI` file | +| `src/CCLIFileWriter.php` | Writes the `CCLI` file | +| `bin/parse-ccli.php` | CLI tool | +| `proto/ccli.proto` | Protobuf schema | +| `generated/Rv/Data/CCLIDocument.php` | Generated message class | + +--- + +## Scope Notes + +The wrapper preserves template data and application metadata by mutating the +generated protobuf in place. It does not inspect or render the slide template. diff --git a/doc/api/clear-groups.md b/doc/api/clear-groups.md new file mode 100644 index 0000000..c46fc67 --- /dev/null +++ b/doc/api/clear-groups.md @@ -0,0 +1,135 @@ +# ClearGroups Library API + +> PHP module for reading and writing the global ProPresenter `ClearGroups` file +> (raw protobuf, no extension) and exposing each clear group definition. + +## Quick Reference + +```php +use ProPresenter\Parser\ClearGroupsFileReader; +use ProPresenter\Parser\ClearGroupsFileWriter; + +$library = ClearGroupsFileReader::read('/path/to/ClearGroups'); + +foreach ($library->getGroups() as $group) { + $group->getName(); // "Alles ausblenden" + $group->getUuid(); // "A91C6AFE-..." + $group->getImageType(); // 11 + $group->getColorHex(); // "#FFFFFF" | null +} + +ClearGroupsFileWriter::write($library, '/path/to/ClearGroups'); +``` + +--- + +## File Layout + +The `ClearGroups` file is the protobuf-serialised +[`ClearGroupsDocument`](../../proto/clearGroups.proto): + +| Field | Type | Description | +|-------|------|-------------| +| `application_info` | `ApplicationInfo` | ProPresenter writer metadata | +| `groups` | repeated `ClearGroupsDocument.ClearGroup` | Clear button definitions | + +Each `ClearGroup` carries: + +| Field | Type | Description | +|-------|------|-------------| +| `uuid` | `UUID` | Stable identifier | +| `name` | string | Display name | +| `layer_targets` | repeated `Action.ClearType` | Layers cleared by the button | +| `is_hidden_in_preview` | bool | Whether preview UI hides the button | +| `image_data` | bytes | Custom icon payload | +| `image_type` | enum | Built-in icon identifier | +| `is_icon_tinted` | bool | Whether `icon_tint_color` applies | +| `icon_tint_color` | `Color` | RGBA float channels in 0..1 | +| `timeline_targets` | repeated `Action.ContentDestination` | Timeline destinations | +| `clear_presentation_next_slide` | bool | Also clear the queued next slide | + +--- + +## Reading + +```php +use ProPresenter\Parser\ClearGroupsFileReader; + +$library = ClearGroupsFileReader::read('/Users/me/.../ClearGroups'); +``` + +Throws `InvalidArgumentException` for missing files and `RuntimeException` for +empty / unreadable files. + +--- + +## ClearGroupsLibrary + +Top-level wrapper around `Rv\Data\ClearGroupsDocument`. Indexes groups by UUID +(case-insensitive) and name. + +```php +$library->getGroups(); // ClearGroupDefinition[] +$library->count(); // int +$library->getClearGroupByUuid('A91C...'); // ?ClearGroupDefinition +$library->getClearGroupByName('Alles ...'); // ?ClearGroupDefinition +$library->addClearGroup('Name', 'UUID'); // ClearGroupDefinition +$library->removeClearGroup('UUID'); // bool +$library->getDocument(); // \Rv\Data\ClearGroupsDocument +``` + +--- + +## ClearGroupDefinition + +```php +$group->getName(); +$group->setName('New Name'); +$group->getUuid(); +$group->setUuid('...'); +$group->getLayerTargets(); +$group->getImageType(); +$group->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null +$group->getColorHex(); // "#RRGGBB" uppercase, alpha dropped, or null +$group->getProto(); // \Rv\Data\ClearGroupsDocument\ClearGroup +``` + +Color channels are floats in 0..1 as ProPresenter stores them. `getColorHex()` +clamps and rounds each channel to 8 bits before formatting. + +--- + +## CLI Tool + +```bash +php bin/parse-clear-groups.php /path/to/ClearGroups +``` + +Output: + +``` +ClearGroups (1): + [1] Alles ausblenden :: A91C6AFE-098F-4559-B2CF-D8373C589589 :: image_type=11 :: #FFFFFF +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/ClearGroupsLibrary.php` | Document-level wrapper with UUID/name lookups | +| `src/ClearGroupDefinition.php` | Single clear group wrapper | +| `src/ClearGroupsFileReader.php` | Reads the `ClearGroups` file | +| `src/ClearGroupsFileWriter.php` | Writes the `ClearGroups` file | +| `bin/parse-clear-groups.php` | CLI tool | +| `proto/clearGroups.proto` | Protobuf schema | +| `generated/Rv/Data/ClearGroupsDocument.php` | Generated message class | + +--- + +## Scope Notes + +The wrapper preserves unknown / uninterpreted protobuf data by mutating the +generated message in place and serialising it back. It does not execute clear +actions or modify slide content. diff --git a/doc/api/communication-devices.md b/doc/api/communication-devices.md new file mode 100644 index 0000000..9504080 --- /dev/null +++ b/doc/api/communication-devices.md @@ -0,0 +1,96 @@ +# CommunicationDevices Library API + +> PHP module for reading and writing the global ProPresenter +> `CommunicationDevices` file. Unlike most config files in this project, this +> file is JSON, not protobuf. + +## Quick Reference + +```php +use ProPresenter\Parser\CommunicationDevice; +use ProPresenter\Parser\CommunicationDevicesFileReader; +use ProPresenter\Parser\CommunicationDevicesFileWriter; + +$library = CommunicationDevicesFileReader::read('/path/to/CommunicationDevices'); +$library->addDevice((new CommunicationDevice())->setId('device-1')->setName('Router')); +CommunicationDevicesFileWriter::write($library, '/path/to/CommunicationDevices'); +``` + +--- + +## File Layout + +`CommunicationDevices` is a JSON array. The reference sample is `[]`, so the +wrapper preserves arbitrary object fields while exposing forward-looking common +fields: `id`, `name`, `type`, and `address`. + +--- + +## Reading + +```php +$library = CommunicationDevicesFileReader::read('/Users/me/.../CommunicationDevices'); +``` + +Throws `InvalidArgumentException` for missing files and `RuntimeException` for +unreadable files or invalid JSON. + +--- + +## CommunicationDevicesLibrary + +```php +CommunicationDevicesLibrary::fromJson($json); // CommunicationDevicesLibrary +$library->toJson(); // string +$library->getDocument(); // raw decoded array +$library->getDevices(); // CommunicationDevice[] +$library->addDevice($device); // CommunicationDevice +$library->removeDevice($id); // bool +$library->count(); // int +``` + +--- + +## CommunicationDevice + +```php +$device->getId(); +$device->setId($id); +$device->getName(); +$device->setName($name); +$device->getType(); +$device->setType($type); +$device->getAddress(); +$device->setAddress($address); +$device->toArray(); // full decoded JSON object +``` + +--- + +## CLI Tool + +```bash +php bin/parse-communication-devices.php /path/to/CommunicationDevices +``` + +Empty files print a useful `(none configured)` summary. + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/CommunicationDevicesLibrary.php` | JSON-array wrapper | +| `src/CommunicationDevice.php` | Single JSON device value object | +| `src/CommunicationDevicesFileReader.php` | Reads and validates JSON | +| `src/CommunicationDevicesFileWriter.php` | Writes compact JSON | +| `bin/parse-communication-devices.php` | CLI summary tool | + +--- + +## Scope Notes + +Because only an empty sample is available, unknown JSON fields are preserved in +each device's backing array. The writer uses compact JSON with unescaped +slashes / Unicode for stable semantic round trips. diff --git a/doc/api/groups.md b/doc/api/groups.md new file mode 100644 index 0000000..d6a13f5 --- /dev/null +++ b/doc/api/groups.md @@ -0,0 +1,172 @@ +# Groups Library API + +> PHP module for the global ProPresenter `Groups` file (raw protobuf, no +> extension). Exposes every named group definition (UUID, name, color, +> hot key) used to organise slides across songs and presentations. + +## Quick Reference + +```php +use ProPresenter\Parser\GroupsFileReader; +use ProPresenter\Parser\GroupsFileWriter; + +$library = GroupsFileReader::read('/path/to/Groups'); + +foreach ($library->getGroups() as $group) { + $group->getName(); // "Verse 1" + $group->getUuid(); // "1D85C82C-EC82-44D8-8ED0-7742D46242C0" + $group->getColorHex(); // "#0077CC" | null +} + +$library->addGroup('Bridge', '...uuid...'); +GroupsFileWriter::write($library, '/path/to/Groups'); +``` + +--- + +## File Layout + +The `Groups` file is the protobuf-serialised +[`ProGroupsDocument`](../../proto/groups.proto): + +| Field | Type | Description | +|-------|------|-------------| +| `groups` | repeated `Group` | Library group definitions (UUID, name, color, hotKey) | + +Each `Group` carries: + +| Field | Type | Description | +|-------|------|-------------| +| `uuid` | `UUID` | Stable identifier referenced by song-level cue groups | +| `name` | string | Display name (e.g. "Verse 1") | +| `color` | `Color` (optional) | RGBA float channels | +| `hotKey` | `HotKey` (optional) | Keyboard shortcut binding | +| `application_group_identifier` | `UUID` (optional) | Parent application group | +| `application_group_name` | string (optional) | Parent application group name | + +Groups are identified by UUID; names should be unique but the format does +not enforce it. + +--- + +## Reading + +```php +use ProPresenter\Parser\GroupsFileReader; + +$library = GroupsFileReader::read('/Users/me/.../Groups'); +``` + +Throws `InvalidArgumentException` for missing files and `RuntimeException` +for empty / unreadable files. + +--- + +## Writing + +```php +use ProPresenter\Parser\GroupsFileWriter; + +GroupsFileWriter::write($library, '/Users/me/.../Groups'); +``` + +The writer serialises the underlying `ProGroupsDocument` back to bytes and +saves them. The unmodified reference sample round-trips byte-for-byte. + +--- + +## GroupLibrary + +Top-level wrapper around `Rv\Data\ProGroupsDocument`. Indexes groups by +UUID (case-insensitive) and by name for fast lookup. + +```php +$library->getGroups(); // GroupDefinition[] +$library->count(); // int +$library->getGroupByUuid('1D85C82C-...'); // ?GroupDefinition (case-insensitive) +$library->getGroupByName('Verse 1'); // ?GroupDefinition + +$library->addGroup('Bridge', '...uuid...'); // GroupDefinition +$library->removeGroup('...uuid...'); // bool + +$library->getDocument(); // \Rv\Data\ProGroupsDocument +``` + +If the same UUID or name appears more than once the first occurrence wins +for lookups; every entry is preserved in `getGroups()` in document order. + +--- + +## GroupDefinition + +```php +$group->getUuid(); // "1D85C82C-EC82-44D8-8ED0-7742D46242C0" +$group->setUuid('...'); // self +$group->getName(); // "Verse 1" +$group->setName('Verse 2'); // self +$group->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null +$group->getColorHex(); // "#0077CC" | null +$group->setColor(['r'=>1, 'g'=>0, 'b'=>0]); // self +$group->getHotKey(); // ?\Rv\Data\HotKey +$group->getApplicationGroupName(); // string +$group->getApplicationGroupUuid(); // string +$group->getProto(); // \Rv\Data\Group (raw protobuf) +``` + +The `GroupDefinition` class name is intentionally distinct from the +existing `Group` class which wraps song-level `CueGroup` objects (slide +references, not library definitions). + +--- + +## CLI Tool + +```bash +php bin/parse-groups.php /path/to/Groups +``` + +Output: + +``` +Groups (29): + [1] Vers :: 4E9D56A2-7E96-4975-97CC-44982257EF8A :: #0077CC + [2] Verse 1 :: 1D85C82C-EC82-44D8-8ED0-7742D46242C0 :: #0077CC + ... +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/GroupLibrary.php` | Document-level wrapper with name / UUID lookup | +| `src/GroupDefinition.php` | Single library group (distinct from `Group` / `CueGroup`) | +| `src/GroupsFileReader.php` | Reads the `Groups` file | +| `src/GroupsFileWriter.php` | Writes the `Groups` file | +| `bin/parse-groups.php` | CLI tool | +| `proto/groups.proto` | Protobuf schema | +| `generated/Rv/Data/ProGroupsDocument.php` | Generated message class | +| `generated/Rv/Data/Group.php` | Generated group message class | + +--- + +## Naming Disambiguation + +The codebase has two `Group`-shaped classes for two different scopes: + +| Class | Scope | Wraps | +|-------|-------|-------| +| `Group` | Song-level slide collection | `Rv\Data\Presentation\CueGroup` | +| `GroupDefinition` | Library-level group definition | `Rv\Data\Group` | + +Songs reference library groups by UUID. The two classes co-exist because +ProPresenter's data model has the same name in both places. + +--- + +## Scope Notes + +This module covers reading and writing the `Groups` document. Wiring up +hot keys to actions and editing application group hierarchies are out of +scope; reach for `getHotKey()` / `getProto()` to inspect them when needed. diff --git a/doc/api/key-mappings.md b/doc/api/key-mappings.md new file mode 100644 index 0000000..bf57ade --- /dev/null +++ b/doc/api/key-mappings.md @@ -0,0 +1,130 @@ +# KeyMappings Library API + +> PHP module for reading and writing the global ProPresenter `KeyMappings` file +> (raw protobuf, no extension) and exposing configured hot-key mappings. + +## Quick Reference + +```php +use ProPresenter\Parser\KeyMappingsFileReader; +use ProPresenter\Parser\KeyMappingsFileWriter; + +$library = KeyMappingsFileReader::read('/path/to/KeyMappings'); + +foreach ($library->getMappings() as $mapping) { + $mapping->getName(); // string + $mapping->getUuid(); // string + $mapping->getHotKey(); // ?\Rv\Data\HotKey + $mapping->getTarget(); // raw bytes +} + +KeyMappingsFileWriter::write($library, '/path/to/KeyMappings'); +``` + +--- + +## File Layout + +The `KeyMappings` file is the protobuf-serialised +[`KeyMappingsDocument`](../../proto/keyMappings.proto): + +| Field | Type | Description | +|-------|------|-------------| +| `application_info` | `ApplicationInfo` | ProPresenter writer metadata | +| `mappings` | repeated `KeyMappingsDocument.Mapping` | Configured key bindings | + +Each `Mapping` carries: + +| Field | Type | Description | +|-------|------|-------------| +| `uuid` | `UUID` | Optional stable mapping identifier | +| `hot_key` | `HotKey` | Key combo that fires the action | +| `target` | bytes | Raw target reference | +| `name` | string | Optional display name | + +--- + +## Reading + +```php +use ProPresenter\Parser\KeyMappingsFileReader; + +$library = KeyMappingsFileReader::read('/Users/me/.../KeyMappings'); +``` + +Throws `InvalidArgumentException` for missing files and `RuntimeException` for +empty / unreadable files. + +--- + +## KeyMappingsLibrary + +Top-level wrapper around `Rv\Data\KeyMappingsDocument`. Indexes mappings by UUID +(case-insensitive) and name. + +```php +$library->getMappings(); +$library->count(); +$library->getMappingByUuid('...'); +$library->getMappingByName('Macro trigger'); +$library->addMapping('Macro trigger', 'UUID', $targetBytes); +$library->removeMapping('UUID'); +$library->getApplicationInfo(); +$library->setApplicationInfo($infoOrNull); +$library->getDocument(); // \Rv\Data\KeyMappingsDocument +``` + +--- + +## KeyMapping + +```php +$mapping->getName(); +$mapping->setName('New Name'); +$mapping->getUuid(); +$mapping->setUuid('...'); +$mapping->getHotKey(); +$mapping->setHotKey($hotKeyOrNull); +$mapping->getTarget(); +$mapping->setTarget($bytes); +$mapping->getProto(); // \Rv\Data\KeyMappingsDocument\Mapping +``` + +Targets are raw bytes because ProPresenter may encode several internal target +types here. Keeping bytes opaque preserves round-trip safety. + +--- + +## CLI Tool + +```bash +php bin/parse-key-mappings.php /path/to/KeyMappings +``` + +Output for the reference sample: + +``` +KeyMappings (0): +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/KeyMappingsLibrary.php` | Document-level wrapper with UUID/name lookups | +| `src/KeyMapping.php` | Single mapping wrapper | +| `src/KeyMappingsFileReader.php` | Reads the `KeyMappings` file | +| `src/KeyMappingsFileWriter.php` | Writes the `KeyMappings` file | +| `bin/parse-key-mappings.php` | CLI tool | +| `proto/keyMappings.proto` | Protobuf schema | +| `generated/Rv/Data/KeyMappingsDocument.php` | Generated message class | + +--- + +## Scope Notes + +The reference sample contains only `ApplicationInfo` and no mappings. The API +still supports mapping additions/removals so configured user files can be edited +and round-tripped safely. diff --git a/doc/api/labels.md b/doc/api/labels.md index 5059a06..b3e28ec 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -7,6 +7,7 @@ ```php use ProPresenter\Parser\LabelsFileReader; +use ProPresenter\Parser\LabelsFileWriter; $library = LabelsFileReader::read('/path/to/Labels'); @@ -16,6 +17,14 @@ foreach ($library->getLabels() as $label) { $label->getColor(); // ['r'=>0.0,'g'=>0.408,'b'=>0.702,'a'=>1.0] | null $label->getColorHex(); // "#0068B3" | null } + +// Modify and persist +$library->addLabel('NewLabel', ['r' => 1.0, 'g' => 0.5, 'b' => 0.0]); +$beamer = $library->getLabelByName('KeyVisual Beamer'); +$beamer?->setColorHex('#FF8800'); +$library->removeLabel('Wiederholen'); + +LabelsFileWriter::write($library, '/path/to/Labels'); ``` --- @@ -54,6 +63,19 @@ empty / unreadable files. --- +## Writing + +```php +use ProPresenter\Parser\LabelsFileWriter; + +LabelsFileWriter::write($library, '/Users/me/.../Labels'); +``` + +Serialises the underlying `ProLabelsDocument` to bytes. The unmodified +reference sample round-trips byte-for-byte. + +--- + ## LabelLibrary Top-level wrapper around `Rv\Data\ProLabelsDocument`. Indexes labels by name @@ -64,7 +86,10 @@ $library->getLabels(); // Label[] $library->count(); // int $library->getLabelByName('Szene 1'); // ?Label (case-sensitive) $library->findLabelByName('szene 1'); // ?Label (case-insensitive) -$library->getDocument(); // \Rv\Data\ProLabelsDocument + +$library->addLabel('NewLabel', ['r'=>1, 'g'=>0, 'b'=>0]); // ?Label +$library->removeLabel('OldLabel'); // bool +$library->getDocument(); // \Rv\Data\ProLabelsDocument ``` If the same name appears more than once in the source document the first @@ -76,11 +101,15 @@ occurrence wins for both lookup helpers; every entry is preserved in ## Label ```php -$label->getName(); // "KeyVisual Beamer" (proto field is `text`) -$label->hasColor(); // bool — was a Color message present? -$label->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null -$label->getColorHex(); // "#RRGGBB" uppercase, alpha dropped, or null -$label->getProto(); // \Rv\Data\Action\Label (raw protobuf) +$label->getName(); // "KeyVisual Beamer" (proto field is `text`) +$label->setName('Renamed'); // self +$label->hasColor(); // bool — was a Color message present? +$label->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null +$label->getColorHex(); // "#RRGGBB" uppercase, alpha dropped, or null +$label->setColor(['r'=>1, 'g'=>0, 'b'=>0]); // self +$label->setColor(null); // clears the color (UI falls back to default) +$label->setColorHex('#FF8800'); // accepts #RRGGBB or #RRGGBBAA +$label->getProto(); // \Rv\Data\Action\Label (raw protobuf) ``` Color channels are floats in 0..1 as ProPresenter stores them. `getColorHex()` @@ -118,9 +147,10 @@ Labels (15): | File | Purpose | |------|---------| -| `src/LabelLibrary.php` | Document-level wrapper with name lookups | -| `src/Label.php` | Single label wrapper (name, color, hex) | +| `src/LabelLibrary.php` | Document-level wrapper with name lookups + add / remove helpers | +| `src/Label.php` | Single label wrapper (name, color, hex) with setters | | `src/LabelsFileReader.php` | Reads the `Labels` file | +| `src/LabelsFileWriter.php` | Writes the `Labels` file | | `bin/parse-labels.php` | CLI tool | | `proto/labels.proto` | Protobuf schema (just imports `Action.Label`) | | `proto/action.proto` | Defines the inner `Action.Label` message | @@ -131,7 +161,6 @@ Labels (15): ## Scope Notes -This module is read-only by design. Writing the `Labels` file back, editing -slide-side label references on `.pro` files, or syncing labels across devices -are not implemented here. Add them by mirroring the `ProFileWriter` / -`ProFileGenerator` pattern when needed. +Editing slide-side label references on `.pro` files (cross-document fan-out) +and syncing labels across devices are out of scope; this module only covers +the global `Labels` document. diff --git a/doc/api/macros.md b/doc/api/macros.md index 6415d56..638e392 100644 --- a/doc/api/macros.md +++ b/doc/api/macros.md @@ -8,18 +8,27 @@ ```php use ProPresenter\Parser\MacrosFileReader; +use ProPresenter\Parser\MacrosFileWriter; $library = MacrosFileReader::read('/path/to/Macros'); foreach ($library->getMacros() as $macro) { - $macro->getName(); // "Gottesdienst START" - $macro->getUuid(); // "FA0602E4-EDA2-4457-BB62-68AA17184217" + $macro->getName(); // "Gottesdienst START" + $macro->getUuid(); // "FA0602E4-EDA2-4457-BB62-68AA17184217" + $macro->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null + $macro->getImageType(); // int — see ImageType enum + $macro->getImageData(); // bytes — custom icon (empty for built-ins) foreach ($library->getCollectionsForMacro($macro) as $collection) { - $collection->getName(); // "Ablauf" - $collection->getUuid(); // "8D02FC57-83F8-4042-9B90-81C229728426" + $collection->getName(); + $collection->getUuid(); } } + +// Modify and persist +$library->addMacro('NewMacro', '...uuid...'); +$library->getMacroByName('NewMacro')?->setColor(['r'=>0.5, 'g'=>0, 'b'=>1]); +MacrosFileWriter::write($library, '/path/to/Macros'); ``` --- @@ -53,6 +62,20 @@ empty / unreadable files. --- +## Writing + +```php +use ProPresenter\Parser\MacrosFileWriter; + +MacrosFileWriter::write($library, '/Users/me/.../Macros'); +``` + +Serialises the underlying `MacrosDocument` to bytes. Round-trip preserves the +overall byte length; field ordering can vary slightly because the protobuf +PHP runtime is not guaranteed to be canonical. + +--- + ## MacroLibrary Top-level wrapper around `Rv\Data\MacrosDocument`. Indexes macros and @@ -71,6 +94,12 @@ $library->getCollectionByName('Ablauf'); // ?MacroCollection $library->getMacrosForCollection($collection); // Macro[] in declared order $library->getCollectionsForMacro($macro); // MacroCollection[] (membership) +// Mutators +$library->addMacro('NewMacro', '...uuid...'); // Macro +$library->removeMacro('...uuid...'); // bool +$library->addCollection('NewCollection', '...uuid...'); // MacroCollection +$library->removeCollection('...uuid...'); // bool + $library->getDocument(); // \Rv\Data\MacrosDocument (raw protobuf) ``` @@ -80,11 +109,18 @@ $library->getDocument(); // \Rv\Data\MacrosDocument (raw protobuf) ```php $macro->getUuid(); // "FA0602E4-..." +$macro->setUuid('...'); // self $macro->getName(); // "Gottesdienst START" +$macro->setName('...'); // self $macro->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null +$macro->setColor(['r'=>0.5,'g'=>0,'b'=>1]); // self (or null to clear) $macro->getTriggerOnStartup(); // bool +$macro->setTriggerOnStartup(true); // self $macro->getActionCount(); // int — number of attached Action entries $macro->getImageType(); // int — see Rv\Data\MacrosDocument\Macro\ImageType +$macro->setImageType(...); // self — pass an ImageType enum value +$macro->getImageData(); // string — custom icon bytes (empty for built-ins) +$macro->setImageData($pngBytes); // self — set a custom icon $macro->getProto(); // \Rv\Data\MacrosDocument\Macro ``` @@ -96,10 +132,14 @@ walk `getActions()` directly when needed. ## MacroCollection ```php -$collection->getUuid(); // "8D02FC57-..." -$collection->getName(); // "Ablauf" -$collection->getMacroUuids(); // string[] — referenced macro UUIDs in order -$collection->getProto(); // \Rv\Data\MacrosDocument\MacroCollection +$collection->getUuid(); // "8D02FC57-..." +$collection->setUuid('...'); // self +$collection->getName(); // "Ablauf" +$collection->setName('...'); // self +$collection->getMacroUuids(); // string[] — referenced macro UUIDs in order +$collection->setMacroUuids(['...']); // self — replace all referenced UUIDs +$collection->addMacroUuid('...'); // self — append a single reference +$collection->getProto(); // \Rv\Data\MacrosDocument\MacroCollection ``` Items use a protobuf `oneof ItemType`; only `macro_id` is currently defined. @@ -132,10 +172,11 @@ Collections (3): | File | Purpose | |------|---------| -| `src/MacroLibrary.php` | Document-level wrapper with lookup helpers | -| `src/Macro.php` | Single macro wrapper | -| `src/MacroCollection.php` | Collection wrapper | +| `src/MacroLibrary.php` | Document-level wrapper with lookup + add / remove helpers | +| `src/Macro.php` | Single macro wrapper with setters | +| `src/MacroCollection.php` | Collection wrapper with setters | | `src/MacrosFileReader.php` | Reads the `Macros` file | +| `src/MacrosFileWriter.php` | Writes the `Macros` file | | `bin/parse-macros.php` | CLI tool | | `proto/macros.proto` | Protobuf schema | | `generated/Rv/Data/MacrosDocument.php` | Generated message classes | @@ -144,8 +185,8 @@ Collections (3): ## Scope Notes -This module is read-only by design. Action editing, slide-side macro -references on `.pro` files (see `Slide::getMacroUuid()` / -`Slide::setMacro()`), and writing the `Macros` file back are not implemented -here. Add them by mirroring the `ProFileWriter` / `ProFileGenerator` pattern -when needed. +Action editing (the inner `repeated Action actions` field on each macro) and +slide-side macro references on `.pro` files (see `Slide::getMacroUuid()` / +`Slide::setMacro()`) are out of scope. This module covers the global +`Macros` document only; reach for `getProto()->getActions()` for raw action +inspection. diff --git a/doc/api/messages.md b/doc/api/messages.md new file mode 100644 index 0000000..3195952 --- /dev/null +++ b/doc/api/messages.md @@ -0,0 +1,110 @@ +# Messages Library API + +> PHP module for reading and writing the global ProPresenter `Messages` file +> (raw protobuf, no extension) and exposing each message definition. + +## Quick Reference + +```php +use ProPresenter\Parser\MessagesFileReader; +use ProPresenter\Parser\MessagesFileWriter; + +$library = MessagesFileReader::read('/path/to/Messages'); + +foreach ($library->getMessages() as $message) { + $message->getTitle(); + $message->getUuid(); + $message->getMessageText(); +} + +$library->addMessage('Lobby Notice', '11111111-1111-1111-1111-111111111111'); +MessagesFileWriter::write($library, '/path/to/Messages'); +``` + +--- + +## File Layout + +The `Messages` file is the protobuf-serialised +[`MessageDocument`](../../proto/messages.proto): + +| Field | Type | Description | +|-------|------|-------------| +| `application_info` | `ApplicationInfo` | ProPresenter metadata | +| `messages` | repeated `Message` | Message definitions in document order | + +--- + +## Reading + +```php +$library = MessagesFileReader::read('/Users/me/.../Messages'); +``` + +Throws `InvalidArgumentException` for missing files and `RuntimeException` for +empty / unreadable files. + +--- + +## MessageLibrary + +```php +$library->getMessages(); // Message[] +$library->count(); // int +$library->getMessageByUuid($uuid); // ?Message (case-insensitive) +$library->getMessageByName($title); // ?Message (case-sensitive title) +$library->addMessage($title, $uuid); // Message +$library->removeMessage($uuid); // bool +$library->getApplicationInfo(); // ?\Rv\Data\ApplicationInfo +$library->getDocument(); // \Rv\Data\MessageDocument +``` + +--- + +## Message + +```php +$message->getUuid(); +$message->setUuid($uuid); +$message->getTitle(); +$message->setTitle($title); +$message->getTimeToRemove(); +$message->setTimeToRemove($seconds); +$message->isVisibleOnNetwork(); +$message->setVisibleOnNetwork(true); +$message->getMessageText(); +$message->setMessageText($text); +$message->getClearType(); +$message->setClearType($enumValue); +$message->getTokens(); // raw repeated Token protos +$message->getProto(); // \Rv\Data\Message +``` + +--- + +## CLI Tool + +```bash +php bin/parse-messages.php /path/to/Messages +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/MessageLibrary.php` | Document wrapper with title / UUID indexes | +| `src/Message.php` | Single message wrapper | +| `src/MessagesFileReader.php` | Reads the `Messages` file | +| `src/MessagesFileWriter.php` | Writes the `Messages` file | +| `bin/parse-messages.php` | CLI summary tool | +| `generated/Rv/Data/MessageDocument.php` | Generated document class | + +--- + +## Scope Notes + +Tokens and token values are preserved as raw generated protobuf objects. The +wrapper exposes them for advanced callers but does not interpret template +rendering semantics. diff --git a/doc/api/props.md b/doc/api/props.md new file mode 100644 index 0000000..7508639 --- /dev/null +++ b/doc/api/props.md @@ -0,0 +1,80 @@ +# Props Library API + +> PHP module for reading, modifying, and writing the global ProPresenter `Props` file. + +## Quick Reference + +```php +use ProPresenter\Parser\PropsFileReader; +use ProPresenter\Parser\PropsFileWriter; + +$library = PropsFileReader::read('/path/to/Props'); + +foreach ($library->getProps() as $prop) { + $prop->getName(); + $prop->getUuid(); + $prop->isEnabled(); +} + +PropsFileWriter::write($library, '/path/to/Props'); +``` + +--- + +## File Layout + +The `Props` file is `Rv\Data\PropDocument` from `propDocument.proto`. +Each prop is stored as a `Rv\Data\Cue` in the document's `cues` field. + +--- + +## PropLibrary + +```php +$library->getDocument(); +$library->getProps(); +$library->getPropByUuid('1FB2...'); // case-insensitive +$library->getPropByName('Props #1'); +$library->addProp($prop); +$library->removeProp($uuid); +$library->count(); +$library->getApplicationInfo(); +``` + +--- + +## Prop + +```php +$prop->getName(); +$prop->setName('Lower Third'); +$prop->getUuid(); +$prop->setUuid('...'); +$prop->isEnabled(); +$prop->setEnabled(true); +$prop->getCompletionTime(); +$prop->getActions(); +$prop->getProto(); +``` + +Use `getProto()` for full Cue/action access. + +--- + +## CLI Tool + +```bash +php bin/parse-props.php /path/to/Props +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/PropsFileReader.php` | Reads the `Props` file | +| `src/PropsFileWriter.php` | Writes the `Props` file | +| `src/PropLibrary.php` | Document wrapper and indexes | +| `src/Prop.php` | Single Cue/prop wrapper | +| `bin/parse-props.php` | CLI summary tool | diff --git a/doc/api/stage.md b/doc/api/stage.md new file mode 100644 index 0000000..d5aa535 --- /dev/null +++ b/doc/api/stage.md @@ -0,0 +1,83 @@ +# Stage Library API + +> PHP module for reading, modifying, and writing the global ProPresenter `Stage` file. + +## Quick Reference + +```php +use ProPresenter\Parser\StageFileReader; +use ProPresenter\Parser\StageFileWriter; + +$library = StageFileReader::read('/path/to/Stage'); + +foreach ($library->getLayouts() as $layout) { + $layout->getName(); + $layout->getUuid(); + $layout->getSlide(); // ?\Rv\Data\Slide +} + +StageFileWriter::write($library, '/path/to/Stage'); +``` + +--- + +## File Layout + +The `Stage` file is the protobuf-serialised `Rv\Data\Stage\Document` from +`stage.proto`. + +| Field | Type | Description | +|-------|------|-------------| +| `application_info` | `ApplicationInfo` | ProPresenter metadata | +| `layouts` | repeated `Stage.Layout` | Stage display layouts | + +--- + +## StageLibrary + +```php +$library->getDocument(); +$library->getLayouts(); +$library->getLayoutByUuid('0455...'); // case-insensitive +$library->getLayoutByName('Default StageDisplay'); +$library->addLayout($layout); +$library->removeLayout($uuid); +$library->count(); +$library->getApplicationInfo(); +``` + +--- + +## StageLayout + +```php +$layout->getName(); +$layout->setName('New name'); +$layout->getUuid(); +$layout->setUuid('...'); +$layout->getSlide(); +$layout->getProto(); +``` + +The slide is exposed as the raw `Rv\Data\Slide` protobuf because stage layouts +can contain complex arrangements. + +--- + +## CLI Tool + +```bash +php bin/parse-stage.php /path/to/Stage +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/StageFileReader.php` | Reads the `Stage` file | +| `src/StageFileWriter.php` | Writes the `Stage` file | +| `src/StageLibrary.php` | Document wrapper and indexes | +| `src/StageLayout.php` | Single layout wrapper | +| `bin/parse-stage.php` | CLI summary tool | diff --git a/doc/api/test-patterns.md b/doc/api/test-patterns.md new file mode 100644 index 0000000..ca4f6ed --- /dev/null +++ b/doc/api/test-patterns.md @@ -0,0 +1,106 @@ +# TestPatterns Library API + +> PHP module for reading and writing the global ProPresenter `TestPatterns` file +> (raw protobuf, no extension), including selected test-pattern state and saved +> pattern definitions. + +## Quick Reference + +```php +use ProPresenter\Parser\TestPatternsFileReader; +use ProPresenter\Parser\TestPatternsFileWriter; + +$library = TestPatternsFileReader::read('/path/to/TestPatterns'); + +$library->getDisplayLocation(); // 3 +$library->getSpecificScreenUuid(); // "BCDE1115-..." +$library->getPatterns(); // TestPatternData[] + +TestPatternsFileWriter::write($library, '/path/to/TestPatterns'); +``` + +--- + +## File Layout + +The `TestPatterns` file is the protobuf-serialised +[`TestPatternDocument`](../../proto/testPattern.proto): + +| Field | Type | Description | +|-------|------|-------------| +| `state` | `TestPatternDocument.TestPatternStateData` | Current test-pattern display state | +| `patterns` | repeated `TestPatternDocument.TestPatternData` | Saved pattern definitions | + +`TestPatternStateData` includes selected pattern UUID/name, display location, +specific screen UUID, identify-screen flag, logo type, and optional user logo. + +--- + +## Reading + +```php +use ProPresenter\Parser\TestPatternsFileReader; + +$library = TestPatternsFileReader::read('/Users/me/.../TestPatterns'); +``` + +Throws `InvalidArgumentException` for missing files and `RuntimeException` for +empty / unreadable files. + +--- + +## TestPatternsLibrary + +Top-level wrapper around `Rv\Data\TestPatternDocument`. Indexes saved pattern +definitions by UUID (case-insensitive) and localization key. + +```php +$library->getState(); +$library->setState($stateOrNull); +$library->getSelectedPatternUuid(); +$library->getSelectedPatternNameLocalizationKey(); +$library->getDisplayLocation(); +$library->getSpecificScreenUuid(); +$library->getPatterns(); +$library->count(); +$library->getPatternByUuid('...'); +$library->getPatternByName('Test Pattern'); +$library->addPattern('Test Pattern', 'UUID'); +$library->removePattern('UUID'); +$library->getDocument(); // \Rv\Data\TestPatternDocument +``` + +--- + +## CLI Tool + +```bash +php bin/parse-test-patterns.php /path/to/TestPatterns +``` + +Output: + +``` +TestPatterns (0): + State: selected=(none) :: name=(none) :: display_location=3 :: screen=BCDE1115-AD40-4BA4-A33A-BFFE3E87223B +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/TestPatternsLibrary.php` | Document-level wrapper with state accessors | +| `src/TestPatternsFileReader.php` | Reads the `TestPatterns` file | +| `src/TestPatternsFileWriter.php` | Writes the `TestPatterns` file | +| `bin/parse-test-patterns.php` | CLI tool | +| `proto/testPattern.proto` | Protobuf schema | +| `generated/Rv/Data/TestPatternDocument.php` | Generated message class | + +--- + +## Scope Notes + +The wrapper exposes `TestPatternData` and `TestPatternStateData` protobufs +directly. It does not render test patterns or interpret nested property oneofs. diff --git a/doc/api/theme.md b/doc/api/theme.md new file mode 100644 index 0000000..fea4e22 --- /dev/null +++ b/doc/api/theme.md @@ -0,0 +1,116 @@ +# Theme Bundle API + +> PHP module for reading, modifying, and writing folder-based ProPresenter themes. + +## Quick Reference + +```php +use ProPresenter\Parser\ThemeFileReader; +use ProPresenter\Parser\ThemeFileWriter; + +$theme = ThemeFileReader::read('/path/to/theme-folder'); + +foreach ($theme->getSlides() as $slide) { + $slide->getName(); // KeyVisual, Liedtext, ... + $slide->getBaseSlide(); // ?\Rv\Data\Slide +} + +foreach ($theme->getAssets() as $asset) { + $asset->getName(); + $asset->getSize(); + $asset->getMimeType(); +} + +ThemeFileWriter::write($theme, '/path/to/output-folder'); +``` + +--- + +## Folder Layout + +A theme is a directory, not a single protobuf file: + +```text +SampleTheme/ +├── Theme # Rv\Data\Template\Document protobuf +└── Assets/ + ├── BACKGROUND.jpg + ├── BAUCHBIND_STREAM.jpg + └── KEY_VISUAL.jpg +``` + +The `Theme` file is a `Rv\Data\Template\Document` from `template.proto`. +Its slides are named theme layouts. + +--- + +## ThemeBundle + +```php +$theme->getDocument(); +$theme->getSlides(); +$theme->getSlideByName('KeyVisual'); +$theme->addSlide($slide); +$theme->removeSlide('KeyVisual'); +$theme->getAssets(); +$theme->getAssetByName('BACKGROUND.jpg'); +$theme->addAsset('NEW.jpg', $bytes); +$theme->removeAsset('NEW.jpg'); +$theme->count(); +$theme->getAssetCount(); +``` + +--- + +## ThemeSlide + +```php +$slide->getName(); +$slide->setName('Liedtext'); +$slide->getBaseSlide(); +$slide->getProto(); +``` + +--- + +## ThemeAsset + +```php +$asset->getName(); +$asset->getBytes(); +$asset->getSize(); +$asset->getMimeType(); // image/jpeg, image/png, ... +``` + +MIME type detection is extension-based and best-effort. + +--- + +## Writing Themes + +`ThemeFileWriter::write()` creates the target folder if needed, writes the +serialized `Theme` protobuf, creates `Assets/`, writes every `ThemeAsset`, and +removes stale files from `Assets/` that are not present in the bundle. + +--- + +## CLI Tool + +```bash +php bin/parse-theme.php /path/to/theme-folder +``` + +The CLI prints slide names plus asset filenames, sizes, and MIME types. + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/ThemeBundle.php` | Top-level theme wrapper | +| `src/ThemeFileReader.php` | Reads a theme folder | +| `src/ThemeFileWriter.php` | Writes a theme folder and cleans stale assets | +| `src/ThemeSlide.php` | Single template slide wrapper | +| `src/ThemeAsset.php` | Single asset value object | +| `bin/parse-theme.php` | CLI summary tool | diff --git a/doc/api/timers.md b/doc/api/timers.md new file mode 100644 index 0000000..c2062f6 --- /dev/null +++ b/doc/api/timers.md @@ -0,0 +1,106 @@ +# Timers Library API + +> PHP module for reading and writing the global ProPresenter `Timers` file +> (raw protobuf, no extension), including the top-level clock format. + +## Quick Reference + +```php +use ProPresenter\Parser\TimersFileReader; + +$library = TimersFileReader::read('/path/to/Timers'); +$library->getClockFormat(); // "HH:mm" + +foreach ($library->getTimers() as $timer) { + $timer->getName(); + $timer->isCountdown(); + $timer->getDurationSeconds(); +} +``` + +--- + +## File Layout + +The `Timers` file is the protobuf-serialised +[`TimersDocument`](../../proto/timers.proto): + +| Field | Type | Description | +|-------|------|-------------| +| `application_info` | `ApplicationInfo` | ProPresenter metadata | +| `clock` | `Clock` | Global clock display settings | +| `timers` | repeated `Timer` | Timer definitions | + +--- + +## Reading + +```php +$library = TimersFileReader::read('/Users/me/.../Timers'); +``` + +Throws `InvalidArgumentException` for missing files and `RuntimeException` for +empty / unreadable files. + +--- + +## TimersLibrary + +```php +$library->getTimers(); // Timer[] +$library->count(); // int +$library->getTimerByUuid($uuid); // ?Timer (case-insensitive) +$library->getTimerByName($name); // ?Timer +$library->addTimer($name, $uuid); // Timer +$library->removeTimer($uuid); // bool +$library->getClockFormat(); // string +$library->setClockFormat('HH:mm'); // void +$library->getApplicationInfo(); // ?\Rv\Data\ApplicationInfo +$library->getDocument(); // \Rv\Data\TimersDocument +``` + +--- + +## Timer + +```php +$timer->getUuid(); +$timer->setUuid($uuid); +$timer->getName(); +$timer->setName($name); +$timer->getConfiguration(); // ?\Rv\Data\Timer\Configuration +$timer->isCountdown(); +$timer->isCountdownToTime(); +$timer->isElapsedTime(); +$timer->getDurationSeconds(); // ?int for countdown timers +$timer->getProto(); // \Rv\Data\Timer +``` + +--- + +## CLI Tool + +```bash +php bin/parse-timers.php /path/to/Timers +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/TimersLibrary.php` | Document wrapper with UUID / name indexes | +| `src/Timer.php` | Single timer wrapper | +| `src/TimersFileReader.php` | Reads the `Timers` file | +| `src/TimersFileWriter.php` | Writes the `Timers` file | +| `bin/parse-timers.php` | CLI summary tool | +| `generated/Rv/Data/TimersDocument.php` | Generated document class | + +--- + +## Scope Notes + +Timer configuration is exposed as the generated protobuf sub-message. Helper +methods cover the oneof timer type and countdown duration without hiding raw +access for callers that need advanced fields. diff --git a/doc/api/workspace.md b/doc/api/workspace.md new file mode 100644 index 0000000..bac30de --- /dev/null +++ b/doc/api/workspace.md @@ -0,0 +1,83 @@ +# Workspace Library API + +> PHP module for reading, modifying, and writing the ProPresenter `Workspace` file. + +## Quick Reference + +```php +use ProPresenter\Parser\WorkspaceFileReader; +use ProPresenter\Parser\WorkspaceFileWriter; + +$library = WorkspaceFileReader::read('/path/to/Workspace'); + +foreach ($library->getScreens() as $screen) { + $screen->getName(); + $screen->getUuid(); + $screen->getScreenType(); +} + +WorkspaceFileWriter::write($library, '/path/to/Workspace'); +``` + +--- + +## File Layout + +The `Workspace` file is `Rv\Data\ProPresenterWorkspace` from `proworkspace.proto`. +Its `pro_screens` entries are `Rv\Data\ProPresenterScreen` messages from +`proscreen.proto`. + +--- + +## WorkspaceLibrary + +```php +$library->getDocument(); +$library->getScreens(); +$library->getScreenByName('StageDisplay'); +$library->getScreenByUuid('C86D...'); // case-insensitive +$library->addScreen($screen); +$library->removeScreen($uuid); +$library->count(); +$library->getAudienceLooks(); +$library->getMasks(); +$library->getVideoInputs(); +$library->getSelectedLibraryName(); +$library->setSelectedLibraryName('Library'); +``` + +--- + +## Screen + +```php +$screen->getName(); +$screen->setName('New name'); +$screen->getUuid(); +$screen->setUuid('...'); +$screen->getScreenType(); +$screen->getIndex(); +$screen->getProto(); +``` + +Use `getProto()` for detailed arrangement, background, and screen geometry data. + +--- + +## CLI Tool + +```bash +php bin/parse-workspace.php /path/to/Workspace +``` + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `src/WorkspaceFileReader.php` | Reads the `Workspace` file | +| `src/WorkspaceFileWriter.php` | Writes the `Workspace` file | +| `src/WorkspaceLibrary.php` | Document wrapper and indexes | +| `src/Screen.php` | Single screen wrapper | +| `bin/parse-workspace.php` | CLI summary tool | diff --git a/doc/keywords.md b/doc/keywords.md index 1db1e96..230842f 100644 --- a/doc/keywords.md +++ b/doc/keywords.md @@ -13,30 +13,34 @@ | ZIP | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | | ZIP64 | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | +| JSON | [api/communication-devices.md](api/communication-devices.md) | ## Song Structure | Keyword | Document | |---------|----------| | song | [api/song.md](api/song.md) | -| group | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 4 | +| group | [api/song.md](api/song.md), [api/groups.md](api/groups.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 4 | | slide | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | | arrangement | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 6 | | translation | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 7 | | verse | [api/song.md](api/song.md) | | chorus | [api/song.md](api/song.md) | | lyrics | [api/song.md](api/song.md) | -| CCLI | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 | +| CCLI | [api/ccli.md](api/ccli.md), [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 | ## Bundle Structure | Keyword | Document | |---------|----------| -| bundle | [api/bundle.md](api/bundle.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | +| bundle | [api/bundle.md](api/bundle.md), [api/theme.md](api/theme.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | probundle | [api/bundle.md](api/bundle.md), [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) | | pro6x | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 1 | | LocalRelativePath | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 | | absolute path | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 2 | +| theme | [api/theme.md](api/theme.md) | +| theme folder | [api/theme.md](api/theme.md) | +| assets | [api/theme.md](api/theme.md), [api/bundle.md](api/bundle.md) | ## Playlist Structure @@ -70,25 +74,86 @@ | Macros file | [api/macros.md](api/macros.md) | | MacroCollection | [api/macros.md](api/macros.md) | | MacroLibrary | [api/macros.md](api/macros.md) | -| media | [api/song.md](api/song.md), [api/bundle.md](api/bundle.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 | -| image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 | +| media | [api/song.md](api/song.md), [api/bundle.md](api/bundle.md), [api/theme.md](api/theme.md) | +| image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3, [api/theme.md](api/theme.md) | | video | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 6 | -| cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | +| cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [api/props.md](api/props.md) | | label | [api/labels.md](api/labels.md), [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | | Labels file | [api/labels.md](api/labels.md) | | LabelLibrary | [api/labels.md](api/labels.md) | | LabelsFileReader | [api/labels.md](api/labels.md) | -| color | [api/labels.md](api/labels.md), [api/macros.md](api/macros.md) | +| color | [api/labels.md](api/labels.md), [api/macros.md](api/macros.md), [api/groups.md](api/groups.md), [api/clear-groups.md](api/clear-groups.md) | + +## Global library files + +| Keyword | Document | +|---------|----------| +| Groups file | [api/groups.md](api/groups.md) | +| GroupLibrary | [api/groups.md](api/groups.md) | +| GroupDefinition | [api/groups.md](api/groups.md) | +| ClearGroups file | [api/clear-groups.md](api/clear-groups.md) | +| ClearGroup | [api/clear-groups.md](api/clear-groups.md) | +| ClearGroupDefinition | [api/clear-groups.md](api/clear-groups.md) | +| CCLI file | [api/ccli.md](api/ccli.md) | +| CCLILibrary | [api/ccli.md](api/ccli.md) | +| copyright | [api/ccli.md](api/ccli.md) | +| license | [api/ccli.md](api/ccli.md) | +| Messages file | [api/messages.md](api/messages.md) | +| MessageLibrary | [api/messages.md](api/messages.md) | +| Message | [api/messages.md](api/messages.md) | +| token | [api/messages.md](api/messages.md) | +| Timers file | [api/timers.md](api/timers.md) | +| TimersLibrary | [api/timers.md](api/timers.md) | +| Timer | [api/timers.md](api/timers.md) | +| Clock | [api/timers.md](api/timers.md) | +| countdown | [api/timers.md](api/timers.md) | +| Stage file | [api/stage.md](api/stage.md) | +| StageLibrary | [api/stage.md](api/stage.md) | +| StageLayout | [api/stage.md](api/stage.md) | +| stage display | [api/stage.md](api/stage.md) | +| Workspace file | [api/workspace.md](api/workspace.md) | +| WorkspaceLibrary | [api/workspace.md](api/workspace.md) | +| Screen | [api/workspace.md](api/workspace.md) | +| audience look | [api/workspace.md](api/workspace.md) | +| mask | [api/workspace.md](api/workspace.md) | +| video input | [api/workspace.md](api/workspace.md) | +| Props file | [api/props.md](api/props.md) | +| PropLibrary | [api/props.md](api/props.md) | +| Prop | [api/props.md](api/props.md) | +| TestPatterns file | [api/test-patterns.md](api/test-patterns.md) | +| TestPatternsLibrary | [api/test-patterns.md](api/test-patterns.md) | +| test pattern | [api/test-patterns.md](api/test-patterns.md) | +| Calendar file | [api/calendar.md](api/calendar.md) | +| CalendarLibrary | [api/calendar.md](api/calendar.md) | +| CalendarEvent | [api/calendar.md](api/calendar.md) | +| schedule | [api/calendar.md](api/calendar.md) | +| KeyMappings file | [api/key-mappings.md](api/key-mappings.md) | +| KeyMappingsLibrary | [api/key-mappings.md](api/key-mappings.md) | +| KeyMapping | [api/key-mappings.md](api/key-mappings.md) | +| hot key | [api/key-mappings.md](api/key-mappings.md), [api/groups.md](api/groups.md) | +| CommunicationDevices file | [api/communication-devices.md](api/communication-devices.md) | +| CommunicationDevicesLibrary | [api/communication-devices.md](api/communication-devices.md) | +| CommunicationDevice | [api/communication-devices.md](api/communication-devices.md) | +| MIDI | [api/communication-devices.md](api/communication-devices.md) | +| OSC | [api/communication-devices.md](api/communication-devices.md) | +| Theme | [api/theme.md](api/theme.md) | +| ThemeBundle | [api/theme.md](api/theme.md) | +| ThemeSlide | [api/theme.md](api/theme.md) | +| ThemeAsset | [api/theme.md](api/theme.md) | ## PHP API | Keyword | Document | |---------|----------| -| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md) | -| write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) | +| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md), [api/groups.md](api/groups.md) | +| write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md), [api/groups.md](api/groups.md), [api/theme.md](api/theme.md) | | generate | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) | -| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md) | +| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md), [api/groups.md](api/groups.md) | | MacrosFileReader | [api/macros.md](api/macros.md) | +| MacrosFileWriter | [api/macros.md](api/macros.md) | +| LabelsFileWriter | [api/labels.md](api/labels.md) | +| GroupsFileReader | [api/groups.md](api/groups.md) | +| GroupsFileWriter | [api/groups.md](api/groups.md) | | ProFileReader | [api/song.md](api/song.md) | | ProFileWriter | [api/song.md](api/song.md) | | ProFileGenerator | [api/song.md](api/song.md) | @@ -98,9 +163,11 @@ | ProBundleReader | [api/bundle.md](api/bundle.md) | | ProBundleWriter | [api/bundle.md](api/bundle.md) | | PresentationBundle | [api/bundle.md](api/bundle.md) | +| ThemeFileReader | [api/theme.md](api/theme.md) | +| ThemeFileWriter | [api/theme.md](api/theme.md) | | Song | [api/song.md](api/song.md) | | PlaylistArchive | [api/playlist.md](api/playlist.md) | -| CLI | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md) | +| CLI | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md), [api/groups.md](api/groups.md) | | command line | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.md), [api/labels.md](api/labels.md) | ## Protobuf @@ -109,13 +176,25 @@ |---------|----------| | Presentation | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 3 | | CueGroup | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 4 | -| Cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | +| Cue | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [api/props.md](api/props.md) | | Action | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 | | Playlist | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 3 | | PlaylistItem | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) Section 5 | | UUID | [formats/pp_song_spec.md](formats/pp_song_spec.md), [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) | | field number | [formats/pp_song_spec.md](formats/pp_song_spec.md) Appendix | | proto | [formats/pp_song_spec.md](formats/pp_song_spec.md) | +| Template.Document | [api/theme.md](api/theme.md) | +| ProPresenterWorkspace | [api/workspace.md](api/workspace.md) | +| ProGroupsDocument | [api/groups.md](api/groups.md) | +| ClearGroupsDocument | [api/clear-groups.md](api/clear-groups.md) | +| MessageDocument | [api/messages.md](api/messages.md) | +| TimersDocument | [api/timers.md](api/timers.md) | +| Stage.Document | [api/stage.md](api/stage.md) | +| PropDocument | [api/props.md](api/props.md) | +| TestPatternDocument | [api/test-patterns.md](api/test-patterns.md) | +| CalendarDocument | [api/calendar.md](api/calendar.md) | +| KeyMappingsDocument | [api/key-mappings.md](api/key-mappings.md) | +| CCLIDocument | [api/ccli.md](api/ccli.md) | ## Troubleshooting diff --git a/doc/reference_samples/CCLI b/doc/reference_samples/CCLI new file mode 100644 index 0000000..3331b01 Binary files /dev/null and b/doc/reference_samples/CCLI differ diff --git a/doc/reference_samples/Calendar b/doc/reference_samples/Calendar new file mode 100644 index 0000000..5ffe6b0 --- /dev/null +++ b/doc/reference_samples/Calendar @@ -0,0 +1,49 @@ + + +& +$3E749EF4-0663-4F0F-AACA-BD801B6D8ACD +Doors Open"*2 ـJBzf"v +t +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:450Hv +t +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer + +& +$794A1711-84DB-42F6-8012-B43B1F395096 +Godi Start"*B{f"w +u +& +$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:027 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ +& +$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:020Hw +u +& +$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:027 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer + +& +$81394F5E-E776-458B-BB06-5C87B3C46D94 +Doors Open"Bzf"v +t +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:450Hv +t +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer \ No newline at end of file diff --git a/doc/reference_samples/ClearGroups b/doc/reference_samples/ClearGroups new file mode 100644 index 0000000..80f7d66 Binary files /dev/null and b/doc/reference_samples/ClearGroups differ diff --git a/doc/reference_samples/CommunicationDevices b/doc/reference_samples/CommunicationDevices new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/doc/reference_samples/CommunicationDevices @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/doc/reference_samples/Groups b/doc/reference_samples/Groups new file mode 100644 index 0000000..f3a22c0 Binary files /dev/null and b/doc/reference_samples/Groups differ diff --git a/doc/reference_samples/KeyMappings b/doc/reference_samples/KeyMappings new file mode 100644 index 0000000..f427670 --- /dev/null +++ b/doc/reference_samples/KeyMappings @@ -0,0 +1,2 @@ + + "" 117899279 \ No newline at end of file diff --git a/doc/reference_samples/Messages b/doc/reference_samples/Messages new file mode 100644 index 0000000..875ac85 Binary files /dev/null and b/doc/reference_samples/Messages differ diff --git a/doc/reference_samples/Props b/doc/reference_samples/Props new file mode 100644 index 0000000..3d0913c Binary files /dev/null and b/doc/reference_samples/Props differ diff --git a/doc/reference_samples/Stage b/doc/reference_samples/Stage new file mode 100644 index 0000000..c9a3d1c Binary files /dev/null and b/doc/reference_samples/Stage differ diff --git a/doc/reference_samples/TestPatterns b/doc/reference_samples/TestPatterns new file mode 100644 index 0000000..ffd3001 --- /dev/null +++ b/doc/reference_samples/TestPatterns @@ -0,0 +1,3 @@ + +*"& +$BCDE1115-AD40-4BA4-A33A-BFFE3E87223B \ No newline at end of file diff --git a/doc/reference_samples/Timers b/doc/reference_samples/Timers new file mode 100644 index 0000000..93316f7 Binary files /dev/null and b/doc/reference_samples/Timers differ diff --git a/doc/reference_samples/Workspace b/doc/reference_samples/Workspace new file mode 100644 index 0000000..df60a03 Binary files /dev/null and b/doc/reference_samples/Workspace differ diff --git a/doc/reference_samples/pp-config/CCLI b/doc/reference_samples/pp-config/CCLI new file mode 100644 index 0000000..3331b01 Binary files /dev/null and b/doc/reference_samples/pp-config/CCLI differ diff --git a/doc/reference_samples/pp-config/Calendar b/doc/reference_samples/pp-config/Calendar new file mode 100644 index 0000000..5ffe6b0 --- /dev/null +++ b/doc/reference_samples/pp-config/Calendar @@ -0,0 +1,49 @@ + + +& +$3E749EF4-0663-4F0F-AACA-BD801B6D8ACD +Doors Open"*2 ـJBzf"v +t +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:450Hv +t +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer + +& +$794A1711-84DB-42F6-8012-B43B1F395096 +Godi Start"*B{f"w +u +& +$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:027 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ +& +$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:020Hw +u +& +$36C9EB9B-E9B0-4D57-95CD-5E4956AC2AF9Godi START - 10:027 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer + +& +$81394F5E-E776-458B-BB06-5C87B3C46D94 +Doors Open"Bzf"v +t +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimerJ +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:450Hv +t +& +$267A1234-C307-4A2A-ADC4-2A1F53583378Doors Open - 9:457 +& +$AD18A4F6-135F-4A52-B92B-CA6619A55A9B AbsoluteTimer \ No newline at end of file diff --git a/doc/reference_samples/pp-config/ClearGroups b/doc/reference_samples/pp-config/ClearGroups new file mode 100644 index 0000000..80f7d66 Binary files /dev/null and b/doc/reference_samples/pp-config/ClearGroups differ diff --git a/doc/reference_samples/pp-config/CommunicationDevices b/doc/reference_samples/pp-config/CommunicationDevices new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/doc/reference_samples/pp-config/CommunicationDevices @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/doc/reference_samples/pp-config/Groups b/doc/reference_samples/pp-config/Groups new file mode 100644 index 0000000..f3a22c0 Binary files /dev/null and b/doc/reference_samples/pp-config/Groups differ diff --git a/doc/reference_samples/pp-config/KeyMappings b/doc/reference_samples/pp-config/KeyMappings new file mode 100644 index 0000000..f427670 --- /dev/null +++ b/doc/reference_samples/pp-config/KeyMappings @@ -0,0 +1,2 @@ + + "" 117899279 \ No newline at end of file diff --git a/doc/reference_samples/pp-config/Labels b/doc/reference_samples/pp-config/Labels new file mode 100644 index 0000000..c3803b9 Binary files /dev/null and b/doc/reference_samples/pp-config/Labels differ diff --git a/doc/reference_samples/pp-config/Macros b/doc/reference_samples/pp-config/Macros new file mode 100644 index 0000000..f49e40e Binary files /dev/null and b/doc/reference_samples/pp-config/Macros differ diff --git a/doc/reference_samples/pp-config/Messages b/doc/reference_samples/pp-config/Messages new file mode 100644 index 0000000..875ac85 Binary files /dev/null and b/doc/reference_samples/pp-config/Messages differ diff --git a/doc/reference_samples/pp-config/Props b/doc/reference_samples/pp-config/Props new file mode 100644 index 0000000..3d0913c Binary files /dev/null and b/doc/reference_samples/pp-config/Props differ diff --git a/doc/reference_samples/pp-config/Stage b/doc/reference_samples/pp-config/Stage new file mode 100644 index 0000000..c9a3d1c Binary files /dev/null and b/doc/reference_samples/pp-config/Stage differ diff --git a/doc/reference_samples/pp-config/TestPatterns b/doc/reference_samples/pp-config/TestPatterns new file mode 100644 index 0000000..ffd3001 --- /dev/null +++ b/doc/reference_samples/pp-config/TestPatterns @@ -0,0 +1,3 @@ + +*"& +$BCDE1115-AD40-4BA4-A33A-BFFE3E87223B \ No newline at end of file diff --git a/doc/reference_samples/pp-config/Timers b/doc/reference_samples/pp-config/Timers new file mode 100644 index 0000000..93316f7 Binary files /dev/null and b/doc/reference_samples/pp-config/Timers differ diff --git a/doc/reference_samples/pp-config/Workspace b/doc/reference_samples/pp-config/Workspace new file mode 100644 index 0000000..df60a03 Binary files /dev/null and b/doc/reference_samples/pp-config/Workspace differ diff --git a/doc/reference_samples/pp-themes/sample/Assets/BACKGROUND.jpg b/doc/reference_samples/pp-themes/sample/Assets/BACKGROUND.jpg new file mode 100644 index 0000000..90faca5 Binary files /dev/null and b/doc/reference_samples/pp-themes/sample/Assets/BACKGROUND.jpg differ diff --git a/doc/reference_samples/pp-themes/sample/Assets/BAUCHBIND_STREAM.jpg b/doc/reference_samples/pp-themes/sample/Assets/BAUCHBIND_STREAM.jpg new file mode 100644 index 0000000..b59b395 Binary files /dev/null and b/doc/reference_samples/pp-themes/sample/Assets/BAUCHBIND_STREAM.jpg differ diff --git a/doc/reference_samples/pp-themes/sample/Assets/KEY_VISUAL.jpg b/doc/reference_samples/pp-themes/sample/Assets/KEY_VISUAL.jpg new file mode 100644 index 0000000..4849d6d Binary files /dev/null and b/doc/reference_samples/pp-themes/sample/Assets/KEY_VISUAL.jpg differ diff --git a/doc/reference_samples/pp-themes/sample/Theme b/doc/reference_samples/pp-themes/sample/Theme new file mode 100644 index 0000000..7d46a9b Binary files /dev/null and b/doc/reference_samples/pp-themes/sample/Theme differ diff --git a/generated/GPBMetadata/Calendar.php b/generated/GPBMetadata/Calendar.php new file mode 100644 index 0000000..49c4a2e --- /dev/null +++ b/generated/GPBMetadata/Calendar.php @@ -0,0 +1,27 @@ +internalAddGeneratedFile( + "\x0A\xF2\x02\x0A\x0Ecalendar.proto\x12\x07rv.data\x1A\x0Auuid.proto\"\x8C\x02\x0A\x10CalendarDocument\x12/\x0A\x06events\x18\x01 \x03(\x0B2\x1F.rv.data.CalendarDocument.Event\x12\x0C\x0A\x04mode\x18\x02 \x01(\x0D\x1A\xB8\x01\x0A\x05Event\x12\x1B\x0A\x04uuid\x18\x01 \x01(\x0B2\x0D.rv.data.UUID\x12\x0C\x0A\x04name\x18\x02 \x01(\x09\x12&\x0A\x0Astart_time\x18\x04 \x01(\x0B2\x12.rv.data.Timestamp\x12\x0D\x0A\x05flags\x18\x05 \x01(\x0C\x12\$\x0A\x08end_time\x18\x06 \x01(\x0B2\x12.rv.data.Timestamp\x12\x13\x0A\x0Baction_data\x18\x08 \x01(\x0C\x12\x12\x0A\x0Amacro_data\x18\x09 \x01(\x0CB4\xF8\x01\x01\xAA\x02\$Pro.SerializationInterop.RVProtoData\xBA\x02\x07RVData_b\x06proto3" + , true); + + static::$is_initialized = true; + } +} + diff --git a/generated/GPBMetadata/KeyMappings.php b/generated/GPBMetadata/KeyMappings.php new file mode 100644 index 0000000..60e986e --- /dev/null +++ b/generated/GPBMetadata/KeyMappings.php @@ -0,0 +1,28 @@ +internalAddGeneratedFile( + "\x0A\xE0\x02\x0A\x11keyMappings.proto\x12\x07rv.data\x1A\x0ChotKey.proto\x1A\x0Auuid.proto\"\xE9\x01\x0A\x13KeyMappingsDocument\x122\x0A\x10application_info\x18\x01 \x01(\x0B2\x18.rv.data.ApplicationInfo\x126\x0A\x08mappings\x18\x02 \x03(\x0B2\$.rv.data.KeyMappingsDocument.Mapping\x1Af\x0A\x07Mapping\x12\x1B\x0A\x04uuid\x18\x01 \x01(\x0B2\x0D.rv.data.UUID\x12 \x0A\x07hot_key\x18\x02 \x01(\x0B2\x0F.rv.data.HotKey\x12\x0E\x0A\x06target\x18\x03 \x01(\x0C\x12\x0C\x0A\x04name\x18\x04 \x01(\x09B4\xF8\x01\x01\xAA\x02\$Pro.SerializationInterop.RVProtoData\xBA\x02\x07RVData_b\x06proto3" + , true); + + static::$is_initialized = true; + } +} + diff --git a/generated/Rv/Data/CalendarDocument.php b/generated/Rv/Data/CalendarDocument.php new file mode 100644 index 0000000..0e10eca --- /dev/null +++ b/generated/Rv/Data/CalendarDocument.php @@ -0,0 +1,107 @@ +rv.data.CalendarDocument + */ +class CalendarDocument extends \Google\Protobuf\Internal\Message +{ + /** + * Events scheduled in the calendar, in the order ProPresenter wrote them. + * + * Generated from protobuf field repeated .rv.data.CalendarDocument.Event events = 1; + */ + private $events; + /** + * Source / mode flag observed in samples (value `1`). Treated as opaque. + * + * Generated from protobuf field uint32 mode = 2; + */ + protected $mode = 0; + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * @type array<\Rv\Data\CalendarDocument\Event>|\Google\Protobuf\Internal\RepeatedField $events + * Events scheduled in the calendar, in the order ProPresenter wrote them. + * @type int $mode + * Source / mode flag observed in samples (value `1`). Treated as opaque. + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Calendar::initOnce(); + parent::__construct($data); + } + + /** + * Events scheduled in the calendar, in the order ProPresenter wrote them. + * + * Generated from protobuf field repeated .rv.data.CalendarDocument.Event events = 1; + * @return \Google\Protobuf\Internal\RepeatedField + */ + public function getEvents() + { + return $this->events; + } + + /** + * Events scheduled in the calendar, in the order ProPresenter wrote them. + * + * Generated from protobuf field repeated .rv.data.CalendarDocument.Event events = 1; + * @param array<\Rv\Data\CalendarDocument\Event>|\Google\Protobuf\Internal\RepeatedField $var + * @return $this + */ + public function setEvents($var) + { + $arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::MESSAGE, \Rv\Data\CalendarDocument\Event::class); + $this->events = $arr; + + return $this; + } + + /** + * Source / mode flag observed in samples (value `1`). Treated as opaque. + * + * Generated from protobuf field uint32 mode = 2; + * @return int + */ + public function getMode() + { + return $this->mode; + } + + /** + * Source / mode flag observed in samples (value `1`). Treated as opaque. + * + * Generated from protobuf field uint32 mode = 2; + * @param int $var + * @return $this + */ + public function setMode($var) + { + GPBUtil::checkUint32($var); + $this->mode = $var; + + return $this; + } + +} + diff --git a/generated/Rv/Data/CalendarDocument/Event.php b/generated/Rv/Data/CalendarDocument/Event.php new file mode 100644 index 0000000..21dd3b2 --- /dev/null +++ b/generated/Rv/Data/CalendarDocument/Event.php @@ -0,0 +1,316 @@ +rv.data.CalendarDocument.Event + */ +class Event extends \Google\Protobuf\Internal\Message +{ + /** + * Stable identity of this calendar event. + * + * Generated from protobuf field .rv.data.UUID uuid = 1; + */ + protected $uuid = null; + /** + * Display name (e.g. "Doors Open"). + * + * Generated from protobuf field string name = 2; + */ + protected $name = ''; + /** + * When the event starts. Stored as Timestamp seconds (and optional nanos). + * + * Generated from protobuf field .rv.data.Timestamp start_time = 4; + */ + protected $start_time = null; + /** + * Opaque flags blob observed in samples. Often a single byte (e.g. 0x01) + * that ProPresenter uses for recurrence or visibility state. + * + * Generated from protobuf field bytes flags = 5; + */ + protected $flags = ''; + /** + * When the event ends. Optional. Same format as `start_time`. + * + * Generated from protobuf field .rv.data.Timestamp end_time = 6; + */ + protected $end_time = null; + /** + * Action that runs when the event fires. Encoded as raw protobuf bytes + * (a `rv.data.Action`-shaped message) so the schema can evolve without + * breaking existing clients. + * + * Generated from protobuf field bytes action_data = 8; + */ + protected $action_data = ''; + /** + * Embedded copy of the macro definition the event triggers. Stored as + * raw protobuf bytes (shape compatible with `MacrosDocument.Macro`). + * + * Generated from protobuf field bytes macro_data = 9; + */ + protected $macro_data = ''; + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * @type \Rv\Data\UUID $uuid + * Stable identity of this calendar event. + * @type string $name + * Display name (e.g. "Doors Open"). + * @type \Rv\Data\Timestamp $start_time + * When the event starts. Stored as Timestamp seconds (and optional nanos). + * @type string $flags + * Opaque flags blob observed in samples. Often a single byte (e.g. 0x01) + * that ProPresenter uses for recurrence or visibility state. + * @type \Rv\Data\Timestamp $end_time + * When the event ends. Optional. Same format as `start_time`. + * @type string $action_data + * Action that runs when the event fires. Encoded as raw protobuf bytes + * (a `rv.data.Action`-shaped message) so the schema can evolve without + * breaking existing clients. + * @type string $macro_data + * Embedded copy of the macro definition the event triggers. Stored as + * raw protobuf bytes (shape compatible with `MacrosDocument.Macro`). + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\Calendar::initOnce(); + parent::__construct($data); + } + + /** + * Stable identity of this calendar event. + * + * Generated from protobuf field .rv.data.UUID uuid = 1; + * @return \Rv\Data\UUID|null + */ + public function getUuid() + { + return $this->uuid; + } + + public function hasUuid() + { + return isset($this->uuid); + } + + public function clearUuid() + { + unset($this->uuid); + } + + /** + * Stable identity of this calendar event. + * + * Generated from protobuf field .rv.data.UUID uuid = 1; + * @param \Rv\Data\UUID $var + * @return $this + */ + public function setUuid($var) + { + GPBUtil::checkMessage($var, \Rv\Data\UUID::class); + $this->uuid = $var; + + return $this; + } + + /** + * Display name (e.g. "Doors Open"). + * + * Generated from protobuf field string name = 2; + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Display name (e.g. "Doors Open"). + * + * Generated from protobuf field string name = 2; + * @param string $var + * @return $this + */ + public function setName($var) + { + GPBUtil::checkString($var, True); + $this->name = $var; + + return $this; + } + + /** + * When the event starts. Stored as Timestamp seconds (and optional nanos). + * + * Generated from protobuf field .rv.data.Timestamp start_time = 4; + * @return \Rv\Data\Timestamp|null + */ + public function getStartTime() + { + return $this->start_time; + } + + public function hasStartTime() + { + return isset($this->start_time); + } + + public function clearStartTime() + { + unset($this->start_time); + } + + /** + * When the event starts. Stored as Timestamp seconds (and optional nanos). + * + * Generated from protobuf field .rv.data.Timestamp start_time = 4; + * @param \Rv\Data\Timestamp $var + * @return $this + */ + public function setStartTime($var) + { + GPBUtil::checkMessage($var, \Rv\Data\Timestamp::class); + $this->start_time = $var; + + return $this; + } + + /** + * Opaque flags blob observed in samples. Often a single byte (e.g. 0x01) + * that ProPresenter uses for recurrence or visibility state. + * + * Generated from protobuf field bytes flags = 5; + * @return string + */ + public function getFlags() + { + return $this->flags; + } + + /** + * Opaque flags blob observed in samples. Often a single byte (e.g. 0x01) + * that ProPresenter uses for recurrence or visibility state. + * + * Generated from protobuf field bytes flags = 5; + * @param string $var + * @return $this + */ + public function setFlags($var) + { + GPBUtil::checkString($var, False); + $this->flags = $var; + + return $this; + } + + /** + * When the event ends. Optional. Same format as `start_time`. + * + * Generated from protobuf field .rv.data.Timestamp end_time = 6; + * @return \Rv\Data\Timestamp|null + */ + public function getEndTime() + { + return $this->end_time; + } + + public function hasEndTime() + { + return isset($this->end_time); + } + + public function clearEndTime() + { + unset($this->end_time); + } + + /** + * When the event ends. Optional. Same format as `start_time`. + * + * Generated from protobuf field .rv.data.Timestamp end_time = 6; + * @param \Rv\Data\Timestamp $var + * @return $this + */ + public function setEndTime($var) + { + GPBUtil::checkMessage($var, \Rv\Data\Timestamp::class); + $this->end_time = $var; + + return $this; + } + + /** + * Action that runs when the event fires. Encoded as raw protobuf bytes + * (a `rv.data.Action`-shaped message) so the schema can evolve without + * breaking existing clients. + * + * Generated from protobuf field bytes action_data = 8; + * @return string + */ + public function getActionData() + { + return $this->action_data; + } + + /** + * Action that runs when the event fires. Encoded as raw protobuf bytes + * (a `rv.data.Action`-shaped message) so the schema can evolve without + * breaking existing clients. + * + * Generated from protobuf field bytes action_data = 8; + * @param string $var + * @return $this + */ + public function setActionData($var) + { + GPBUtil::checkString($var, False); + $this->action_data = $var; + + return $this; + } + + /** + * Embedded copy of the macro definition the event triggers. Stored as + * raw protobuf bytes (shape compatible with `MacrosDocument.Macro`). + * + * Generated from protobuf field bytes macro_data = 9; + * @return string + */ + public function getMacroData() + { + return $this->macro_data; + } + + /** + * Embedded copy of the macro definition the event triggers. Stored as + * raw protobuf bytes (shape compatible with `MacrosDocument.Macro`). + * + * Generated from protobuf field bytes macro_data = 9; + * @param string $var + * @return $this + */ + public function setMacroData($var) + { + GPBUtil::checkString($var, False); + $this->macro_data = $var; + + return $this; + } + +} + diff --git a/generated/Rv/Data/KeyMappingsDocument.php b/generated/Rv/Data/KeyMappingsDocument.php new file mode 100644 index 0000000..2b4b076 --- /dev/null +++ b/generated/Rv/Data/KeyMappingsDocument.php @@ -0,0 +1,115 @@ +rv.data.KeyMappingsDocument + */ +class KeyMappingsDocument extends \Google\Protobuf\Internal\Message +{ + /** + * Application metadata of the writer. + * + * Generated from protobuf field .rv.data.ApplicationInfo application_info = 1; + */ + protected $application_info = null; + /** + * Configured key bindings. Empty in the reference sample. + * + * Generated from protobuf field repeated .rv.data.KeyMappingsDocument.Mapping mappings = 2; + */ + private $mappings; + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * @type \Rv\Data\ApplicationInfo $application_info + * Application metadata of the writer. + * @type array<\Rv\Data\KeyMappingsDocument\Mapping>|\Google\Protobuf\Internal\RepeatedField $mappings + * Configured key bindings. Empty in the reference sample. + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\KeyMappings::initOnce(); + parent::__construct($data); + } + + /** + * Application metadata of the writer. + * + * Generated from protobuf field .rv.data.ApplicationInfo application_info = 1; + * @return \Rv\Data\ApplicationInfo|null + */ + public function getApplicationInfo() + { + return $this->application_info; + } + + public function hasApplicationInfo() + { + return isset($this->application_info); + } + + public function clearApplicationInfo() + { + unset($this->application_info); + } + + /** + * Application metadata of the writer. + * + * Generated from protobuf field .rv.data.ApplicationInfo application_info = 1; + * @param \Rv\Data\ApplicationInfo $var + * @return $this + */ + public function setApplicationInfo($var) + { + GPBUtil::checkMessage($var, \Rv\Data\ApplicationInfo::class); + $this->application_info = $var; + + return $this; + } + + /** + * Configured key bindings. Empty in the reference sample. + * + * Generated from protobuf field repeated .rv.data.KeyMappingsDocument.Mapping mappings = 2; + * @return \Google\Protobuf\Internal\RepeatedField + */ + public function getMappings() + { + return $this->mappings; + } + + /** + * Configured key bindings. Empty in the reference sample. + * + * Generated from protobuf field repeated .rv.data.KeyMappingsDocument.Mapping mappings = 2; + * @param array<\Rv\Data\KeyMappingsDocument\Mapping>|\Google\Protobuf\Internal\RepeatedField $var + * @return $this + */ + public function setMappings($var) + { + $arr = GPBUtil::checkRepeatedField($var, \Google\Protobuf\Internal\GPBType::MESSAGE, \Rv\Data\KeyMappingsDocument\Mapping::class); + $this->mappings = $arr; + + return $this; + } + +} + diff --git a/generated/Rv/Data/KeyMappingsDocument/Mapping.php b/generated/Rv/Data/KeyMappingsDocument/Mapping.php new file mode 100644 index 0000000..813a543 --- /dev/null +++ b/generated/Rv/Data/KeyMappingsDocument/Mapping.php @@ -0,0 +1,196 @@ +rv.data.KeyMappingsDocument.Mapping + */ +class Mapping extends \Google\Protobuf\Internal\Message +{ + /** + * Optional stable identifier for the mapping. + * + * Generated from protobuf field .rv.data.UUID uuid = 1; + */ + protected $uuid = null; + /** + * The hot key combo that fires the action. + * + * Generated from protobuf field .rv.data.HotKey hot_key = 2; + */ + protected $hot_key = null; + /** + * Action target — typically a macro UUID, a control identifier, or any + * other reference ProPresenter chooses to encode. Kept as bytes so we + * round-trip cleanly while ProPresenter's internal schema evolves. + * + * Generated from protobuf field bytes target = 3; + */ + protected $target = ''; + /** + * Display name (optional). + * + * Generated from protobuf field string name = 4; + */ + protected $name = ''; + + /** + * Constructor. + * + * @param array $data { + * Optional. Data for populating the Message object. + * + * @type \Rv\Data\UUID $uuid + * Optional stable identifier for the mapping. + * @type \Rv\Data\HotKey $hot_key + * The hot key combo that fires the action. + * @type string $target + * Action target — typically a macro UUID, a control identifier, or any + * other reference ProPresenter chooses to encode. Kept as bytes so we + * round-trip cleanly while ProPresenter's internal schema evolves. + * @type string $name + * Display name (optional). + * } + */ + public function __construct($data = NULL) { + \GPBMetadata\KeyMappings::initOnce(); + parent::__construct($data); + } + + /** + * Optional stable identifier for the mapping. + * + * Generated from protobuf field .rv.data.UUID uuid = 1; + * @return \Rv\Data\UUID|null + */ + public function getUuid() + { + return $this->uuid; + } + + public function hasUuid() + { + return isset($this->uuid); + } + + public function clearUuid() + { + unset($this->uuid); + } + + /** + * Optional stable identifier for the mapping. + * + * Generated from protobuf field .rv.data.UUID uuid = 1; + * @param \Rv\Data\UUID $var + * @return $this + */ + public function setUuid($var) + { + GPBUtil::checkMessage($var, \Rv\Data\UUID::class); + $this->uuid = $var; + + return $this; + } + + /** + * The hot key combo that fires the action. + * + * Generated from protobuf field .rv.data.HotKey hot_key = 2; + * @return \Rv\Data\HotKey|null + */ + public function getHotKey() + { + return $this->hot_key; + } + + public function hasHotKey() + { + return isset($this->hot_key); + } + + public function clearHotKey() + { + unset($this->hot_key); + } + + /** + * The hot key combo that fires the action. + * + * Generated from protobuf field .rv.data.HotKey hot_key = 2; + * @param \Rv\Data\HotKey $var + * @return $this + */ + public function setHotKey($var) + { + GPBUtil::checkMessage($var, \Rv\Data\HotKey::class); + $this->hot_key = $var; + + return $this; + } + + /** + * Action target — typically a macro UUID, a control identifier, or any + * other reference ProPresenter chooses to encode. Kept as bytes so we + * round-trip cleanly while ProPresenter's internal schema evolves. + * + * Generated from protobuf field bytes target = 3; + * @return string + */ + public function getTarget() + { + return $this->target; + } + + /** + * Action target — typically a macro UUID, a control identifier, or any + * other reference ProPresenter chooses to encode. Kept as bytes so we + * round-trip cleanly while ProPresenter's internal schema evolves. + * + * Generated from protobuf field bytes target = 3; + * @param string $var + * @return $this + */ + public function setTarget($var) + { + GPBUtil::checkString($var, False); + $this->target = $var; + + return $this; + } + + /** + * Display name (optional). + * + * Generated from protobuf field string name = 4; + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Display name (optional). + * + * Generated from protobuf field string name = 4; + * @param string $var + * @return $this + */ + public function setName($var) + { + GPBUtil::checkString($var, True); + $this->name = $var; + + return $this; + } + +} + diff --git a/proto/calendar.proto b/proto/calendar.proto new file mode 100644 index 0000000..a0c4163 --- /dev/null +++ b/proto/calendar.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +package rv.data; + +option cc_enable_arenas = true; +option csharp_namespace = "Pro.SerializationInterop.RVProtoData"; +option swift_prefix = "RVData_"; + +import "rvtimestamp.proto"; +import "uuid.proto"; + +// CalendarDocument is the global ProPresenter `Calendar` file. It holds +// scheduled events that fire macros at given times. The exact semantics of +// the inner sub-messages 8 and 9 (action and embedded macro reference) are +// preserved as raw bytes so the document round-trips byte-for-byte; clients +// that need to inspect them can decode them with `protoc --decode_raw` or +// reach for the raw protobuf message. +message CalendarDocument { + message Event { + // Stable identity of this calendar event. + .rv.data.UUID uuid = 1; + + // Display name (e.g. "Doors Open"). + string name = 2; + + // When the event starts. Stored as Timestamp seconds (and optional nanos). + .rv.data.Timestamp start_time = 4; + + // Opaque flags blob observed in samples. Often a single byte (e.g. 0x01) + // that ProPresenter uses for recurrence or visibility state. + bytes flags = 5; + + // When the event ends. Optional. Same format as `start_time`. + .rv.data.Timestamp end_time = 6; + + // Action that runs when the event fires. Encoded as raw protobuf bytes + // (a `rv.data.Action`-shaped message) so the schema can evolve without + // breaking existing clients. + bytes action_data = 8; + + // Embedded copy of the macro definition the event triggers. Stored as + // raw protobuf bytes (shape compatible with `MacrosDocument.Macro`). + bytes macro_data = 9; + } + + // Events scheduled in the calendar, in the order ProPresenter wrote them. + repeated Event events = 1; + + // Source / mode flag observed in samples (value `1`). Treated as opaque. + uint32 mode = 2; +} diff --git a/proto/keyMappings.proto b/proto/keyMappings.proto new file mode 100644 index 0000000..cfc3414 --- /dev/null +++ b/proto/keyMappings.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +package rv.data; + +option cc_enable_arenas = true; +option csharp_namespace = "Pro.SerializationInterop.RVProtoData"; +option swift_prefix = "RVData_"; + +import "applicationInfo.proto"; +import "hotKey.proto"; +import "uuid.proto"; + +// KeyMappingsDocument is the global ProPresenter `KeyMappings` file. The +// reference sample on disk is just an `ApplicationInfo` envelope with an +// otherwise empty body — ProPresenter seeds the file at startup. Once the +// user binds hot keys we expect them in `mappings`. +message KeyMappingsDocument { + message Mapping { + // Optional stable identifier for the mapping. + .rv.data.UUID uuid = 1; + + // The hot key combo that fires the action. + .rv.data.HotKey hot_key = 2; + + // Action target — typically a macro UUID, a control identifier, or any + // other reference ProPresenter chooses to encode. Kept as bytes so we + // round-trip cleanly while ProPresenter's internal schema evolves. + bytes target = 3; + + // Display name (optional). + string name = 4; + } + + // Application metadata of the writer. + .rv.data.ApplicationInfo application_info = 1; + + // Configured key bindings. Empty in the reference sample. + repeated Mapping mappings = 2; +} diff --git a/src/CCLIFileReader.php b/src/CCLIFileReader.php new file mode 100644 index 0000000..b95d1a7 --- /dev/null +++ b/src/CCLIFileReader.php @@ -0,0 +1,38 @@ +mergeFromString($data); + + return new CCLILibrary($document); + } +} diff --git a/src/CCLIFileWriter.php b/src/CCLIFileWriter.php new file mode 100644 index 0000000..677130a --- /dev/null +++ b/src/CCLIFileWriter.php @@ -0,0 +1,26 @@ +getDocument()->serializeToString(); + $writtenBytes = file_put_contents($filePath, $data); + + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write CCLI file: %s', $filePath)); + } + } +} diff --git a/src/CCLILibrary.php b/src/CCLILibrary.php new file mode 100644 index 0000000..bff728b --- /dev/null +++ b/src/CCLILibrary.php @@ -0,0 +1,103 @@ +document->getEnableCcliDisplay(); + } + + public function setCCLIDisplayEnabled(bool $enabled): self + { + $this->document->setEnableCcliDisplay($enabled); + + return $this; + } + + public function getCCLILicense(): string + { + return $this->document->getCcliLicense(); + } + + public function setCCLILicense(string $license): self + { + $this->document->setCcliLicense($license); + + return $this; + } + + public function getDisplayType(): int + { + return $this->document->getDisplayType(); + } + + public function setDisplayType(int $displayType): self + { + $this->document->setDisplayType($displayType); + + return $this; + } + + public function getTemplate(): ?Slide + { + if (!$this->document->hasTemplate()) { + return null; + } + + return $this->document->getTemplate(); + } + + public function setTemplate(?Slide $template): self + { + if ($template === null) { + $this->document->clearTemplate(); + + return $this; + } + + $this->document->setTemplate($template); + + return $this; + } + + public function getApplicationInfo(): ?ApplicationInfo + { + return $this->document->getApplicationInfo(); + } + + public function setApplicationInfo(?ApplicationInfo $applicationInfo): self + { + if ($applicationInfo === null) { + $this->document->clearApplicationInfo(); + + return $this; + } + + $this->document->setApplicationInfo($applicationInfo); + + return $this; + } + + public function getDocument(): CCLIDocument + { + return $this->document; + } +} diff --git a/src/CalendarEvent.php b/src/CalendarEvent.php new file mode 100644 index 0000000..57d6fc6 --- /dev/null +++ b/src/CalendarEvent.php @@ -0,0 +1,140 @@ +event->getUuid()?->getString() ?? ''; + } + + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->event->setUuid($proto); + + return $this; + } + + public function getName(): string + { + return $this->event->getName(); + } + + public function setName(string $name): self + { + $this->event->setName($name); + + return $this; + } + + public function getStartTime(): ?Timestamp + { + return $this->event->getStartTime(); + } + + public function setStartTime(Timestamp $timestamp): self + { + $this->event->setStartTime($timestamp); + + return $this; + } + + public function getStartTimeSeconds(): ?int + { + $seconds = $this->event->getStartTime()?->getSeconds(); + + return $seconds === null ? null : (int) $seconds; + } + + public function setStartTimeSeconds(int $seconds): self + { + $timestamp = new Timestamp(); + $timestamp->setSeconds($seconds); + $this->event->setStartTime($timestamp); + + return $this; + } + + public function getEndTime(): ?Timestamp + { + return $this->event->getEndTime(); + } + + public function setEndTime(Timestamp $timestamp): self + { + $this->event->setEndTime($timestamp); + + return $this; + } + + public function getEndTimeSeconds(): ?int + { + $seconds = $this->event->getEndTime()?->getSeconds(); + + return $seconds === null ? null : (int) $seconds; + } + + public function setEndTimeSeconds(int $seconds): self + { + $timestamp = new Timestamp(); + $timestamp->setSeconds($seconds); + $this->event->setEndTime($timestamp); + + return $this; + } + + public function getFlags(): string + { + return $this->event->getFlags(); + } + + public function setFlags(string $flags): self + { + $this->event->setFlags($flags); + + return $this; + } + + public function getActionData(): string + { + return $this->event->getActionData(); + } + + public function setActionData(string $actionData): self + { + $this->event->setActionData($actionData); + + return $this; + } + + public function getMacroData(): string + { + return $this->event->getMacroData(); + } + + public function setMacroData(string $macroData): self + { + $this->event->setMacroData($macroData); + + return $this; + } + + public function getProto(): EventProto + { + return $this->event; + } +} diff --git a/src/CalendarFileReader.php b/src/CalendarFileReader.php new file mode 100644 index 0000000..7c7cc1a --- /dev/null +++ b/src/CalendarFileReader.php @@ -0,0 +1,37 @@ +mergeFromString($data); + + return new CalendarLibrary($document); + } +} diff --git a/src/CalendarFileWriter.php b/src/CalendarFileWriter.php new file mode 100644 index 0000000..5a1c0ab --- /dev/null +++ b/src/CalendarFileWriter.php @@ -0,0 +1,24 @@ +getDocument()->serializeToString()); + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write Calendar file: %s', $filePath)); + } + } +} diff --git a/src/CalendarLibrary.php b/src/CalendarLibrary.php new file mode 100644 index 0000000..6332e42 --- /dev/null +++ b/src/CalendarLibrary.php @@ -0,0 +1,127 @@ + */ + private array $eventsByUuid = []; + + /** @var array */ + private array $eventsByName = []; + + public function __construct( + private readonly CalendarDocument $document, + ) { + $this->rebuildIndex(); + } + + /** + * @return CalendarEvent[] + */ + public function getEvents(): array + { + return $this->events; + } + + public function count(): int + { + return count($this->events); + } + + public function getEventByUuid(string $uuid): ?CalendarEvent + { + return $this->eventsByUuid[strtoupper($uuid)] ?? null; + } + + public function getEventByName(string $name): ?CalendarEvent + { + return $this->eventsByName[$name] ?? null; + } + + public function addEvent(string $name, string $uuid): CalendarEvent + { + $proto = new EventProto(); + $uuidProto = new UUID(); + $uuidProto->setString($uuid); + $proto->setUuid($uuidProto); + $proto->setName($name); + + $existing = iterator_to_array($this->document->getEvents()); + $existing[] = $proto; + $this->document->setEvents($existing); + $this->rebuildIndex(); + + return $this->getEventByUuid($uuid) ?? new CalendarEvent($proto); + } + + public function removeEvent(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getEvents() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setEvents($kept); + $this->rebuildIndex(); + + return true; + } + + public function getMode(): int + { + return $this->document->getMode(); + } + + public function setMode(int $mode): void + { + $this->document->setMode($mode); + } + + public function getDocument(): CalendarDocument + { + return $this->document; + } + + private function rebuildIndex(): void + { + $this->events = []; + $this->eventsByUuid = []; + $this->eventsByName = []; + + foreach ($this->document->getEvents() as $proto) { + $event = new CalendarEvent($proto); + $this->events[] = $event; + + $uuid = strtoupper($event->getUuid()); + if ($uuid !== '') { + $this->eventsByUuid[$uuid] = $event; + } + + $name = $event->getName(); + if ($name !== '') { + $this->eventsByName[$name] ??= $event; + } + } + } +} diff --git a/src/ClearGroupDefinition.php b/src/ClearGroupDefinition.php new file mode 100644 index 0000000..5f894c0 --- /dev/null +++ b/src/ClearGroupDefinition.php @@ -0,0 +1,199 @@ +group->getUuid()?->getString() ?? ''; + } + + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->group->setUuid($proto); + + return $this; + } + + public function getName(): string + { + return $this->group->getName(); + } + + public function setName(string $name): self + { + $this->group->setName($name); + + return $this; + } + + /** + * @return int[] + */ + public function getLayerTargets(): array + { + return iterator_to_array($this->group->getLayerTargets()); + } + + /** + * @param int[] $targets + */ + public function setLayerTargets(array $targets): self + { + $this->group->setLayerTargets($targets); + + return $this; + } + + public function isHiddenInPreview(): bool + { + return $this->group->getIsHiddenInPreview(); + } + + public function setHiddenInPreview(bool $hidden): self + { + $this->group->setIsHiddenInPreview($hidden); + + return $this; + } + + public function getImageData(): string + { + return $this->group->getImageData(); + } + + public function setImageData(string $imageData): self + { + $this->group->setImageData($imageData); + + return $this; + } + + public function getImageType(): int + { + return $this->group->getImageType(); + } + + public function setImageType(int $imageType): self + { + $this->group->setImageType($imageType); + + return $this; + } + + public function isIconTinted(): bool + { + return $this->group->getIsIconTinted(); + } + + public function setIconTinted(bool $tinted): self + { + $this->group->setIsIconTinted($tinted); + + return $this; + } + + /** + * @return array{r: float, g: float, b: float, a: float}|null + */ + public function getColor(): ?array + { + if (!$this->group->hasIconTintColor()) { + return null; + } + + $color = $this->group->getIconTintColor(); + + return [ + 'r' => $color->getRed(), + 'g' => $color->getGreen(), + 'b' => $color->getBlue(), + 'a' => $color->getAlpha(), + ]; + } + + public function getColorHex(): ?string + { + $color = $this->getColor(); + if ($color === null) { + return null; + } + + return sprintf( + '#%02X%02X%02X', + (int) round(max(0.0, min(1.0, $color['r'])) * 255), + (int) round(max(0.0, min(1.0, $color['g'])) * 255), + (int) round(max(0.0, min(1.0, $color['b'])) * 255), + ); + } + + /** + * @param array{r: float, g: float, b: float, a?: float}|null $color + */ + public function setColor(?array $color): self + { + if ($color === null) { + $this->group->clearIconTintColor(); + + return $this; + } + + $proto = new Color(); + $proto->setRed((float) $color['r']); + $proto->setGreen((float) $color['g']); + $proto->setBlue((float) $color['b']); + $proto->setAlpha((float) ($color['a'] ?? 1.0)); + $this->group->setIconTintColor($proto); + + return $this; + } + + /** + * @return int[] + */ + public function getTimelineTargets(): array + { + return iterator_to_array($this->group->getTimelineTargets()); + } + + /** + * @param int[] $targets + */ + public function setTimelineTargets(array $targets): self + { + $this->group->setTimelineTargets($targets); + + return $this; + } + + public function clearsPresentationNextSlide(): bool + { + return $this->group->getClearPresentationNextSlide(); + } + + public function setClearPresentationNextSlide(bool $clear): self + { + $this->group->setClearPresentationNextSlide($clear); + + return $this; + } + + public function getProto(): ClearGroupProto + { + return $this->group; + } +} diff --git a/src/ClearGroupsFileReader.php b/src/ClearGroupsFileReader.php new file mode 100644 index 0000000..bb06635 --- /dev/null +++ b/src/ClearGroupsFileReader.php @@ -0,0 +1,38 @@ +mergeFromString($data); + + return new ClearGroupsLibrary($document); + } +} diff --git a/src/ClearGroupsFileWriter.php b/src/ClearGroupsFileWriter.php new file mode 100644 index 0000000..06e6608 --- /dev/null +++ b/src/ClearGroupsFileWriter.php @@ -0,0 +1,26 @@ +getDocument()->serializeToString(); + $writtenBytes = file_put_contents($filePath, $data); + + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write ClearGroups file: %s', $filePath)); + } + } +} diff --git a/src/ClearGroupsLibrary.php b/src/ClearGroupsLibrary.php new file mode 100644 index 0000000..7678275 --- /dev/null +++ b/src/ClearGroupsLibrary.php @@ -0,0 +1,119 @@ + */ + private array $groupsByUuid = []; + + /** @var array */ + private array $groupsByName = []; + + public function __construct( + private readonly ClearGroupsDocument $document, + ) { + $this->rebuildIndex(); + } + + /** + * Return clear groups in document order. + * + * @return ClearGroupDefinition[] + */ + public function getGroups(): array + { + return $this->groups; + } + + public function count(): int + { + return count($this->groups); + } + + public function getClearGroupByUuid(string $uuid): ?ClearGroupDefinition + { + return $this->groupsByUuid[strtoupper($uuid)] ?? null; + } + + public function getClearGroupByName(string $name): ?ClearGroupDefinition + { + return $this->groupsByName[$name] ?? null; + } + + public function addClearGroup(string $name, string $uuid): ClearGroupDefinition + { + $proto = new ClearGroupProto(); + $uuidProto = new UUID(); + $uuidProto->setString($uuid); + $proto->setUuid($uuidProto); + $proto->setName($name); + + $existing = iterator_to_array($this->document->getGroups()); + $existing[] = $proto; + $this->document->setGroups($existing); + $this->rebuildIndex(); + + return $this->getClearGroupByUuid($uuid) ?? new ClearGroupDefinition($proto); + } + + public function removeClearGroup(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getGroups() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setGroups($kept); + $this->rebuildIndex(); + + return true; + } + + public function getDocument(): ClearGroupsDocument + { + return $this->document; + } + + private function rebuildIndex(): void + { + $this->groups = []; + $this->groupsByUuid = []; + $this->groupsByName = []; + + foreach ($this->document->getGroups() as $proto) { + $group = new ClearGroupDefinition($proto); + $this->groups[] = $group; + + $uuid = strtoupper($group->getUuid()); + if ($uuid !== '') { + $this->groupsByUuid[$uuid] = $group; + } + + $name = $group->getName(); + if ($name !== '') { + $this->groupsByName[$name] ??= $group; + } + } + } +} diff --git a/src/CommunicationDevice.php b/src/CommunicationDevice.php new file mode 100644 index 0000000..cd1f248 --- /dev/null +++ b/src/CommunicationDevice.php @@ -0,0 +1,75 @@ + */ + private array $fields; + + /** + * @param array|null $fields + */ + public function __construct(?array $fields = null) + { + $this->fields = $fields ?? []; + } + + public function getId(): string + { + return (string) ($this->fields['id'] ?? ''); + } + + public function setId(string $id): self + { + $this->fields['id'] = $id; + + return $this; + } + + public function getName(): string + { + return (string) ($this->fields['name'] ?? ''); + } + + public function setName(string $name): self + { + $this->fields['name'] = $name; + + return $this; + } + + public function getType(): string + { + return (string) ($this->fields['type'] ?? ''); + } + + public function setType(string $type): self + { + $this->fields['type'] = $type; + + return $this; + } + + public function getAddress(): string + { + return (string) ($this->fields['address'] ?? ''); + } + + public function setAddress(string $address): self + { + $this->fields['address'] = $address; + + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->fields; + } +} diff --git a/src/CommunicationDevicesFileReader.php b/src/CommunicationDevicesFileReader.php new file mode 100644 index 0000000..cb8e374 --- /dev/null +++ b/src/CommunicationDevicesFileReader.php @@ -0,0 +1,25 @@ +toJson(JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $writtenBytes = file_put_contents($filePath, $json); + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write CommunicationDevices file: %s', $filePath)); + } + } +} diff --git a/src/CommunicationDevicesLibrary.php b/src/CommunicationDevicesLibrary.php new file mode 100644 index 0000000..7996eae --- /dev/null +++ b/src/CommunicationDevicesLibrary.php @@ -0,0 +1,94 @@ +devices = array_values($devices); + } + + public static function fromJson(string $json): self + { + $decoded = json_decode($json, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Unable to decode CommunicationDevices JSON: ' . json_last_error_msg()); + } + + if (!is_array($decoded)) { + throw new RuntimeException('CommunicationDevices JSON must decode to an array.'); + } + + $devices = []; + foreach ($decoded as $entry) { + if (is_array($entry)) { + /** @var array $entry */ + $devices[] = new CommunicationDevice($entry); + } + } + + return new self($devices); + } + + public function toJson(int $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES): string + { + $json = json_encode($this->getDocument(), $flags); + if ($json === false) { + throw new RuntimeException('Unable to encode CommunicationDevices JSON: ' . json_last_error_msg()); + } + + return $json; + } + + /** + * @return array> + */ + public function getDocument(): array + { + return array_map(static fn (CommunicationDevice $device): array => $device->toArray(), $this->devices); + } + + /** + * @return CommunicationDevice[] + */ + public function getDevices(): array + { + return $this->devices; + } + + public function addDevice(CommunicationDevice $device): CommunicationDevice + { + $this->devices[] = $device; + + return $device; + } + + public function removeDevice(string $id): bool + { + foreach ($this->devices as $index => $device) { + if ($device->getId() === $id) { + array_splice($this->devices, $index, 1); + + return true; + } + } + + return false; + } + + public function count(): int + { + return count($this->devices); + } +} diff --git a/src/GroupDefinition.php b/src/GroupDefinition.php new file mode 100644 index 0000000..b17bad8 --- /dev/null +++ b/src/GroupDefinition.php @@ -0,0 +1,119 @@ +group->getUuid()?->getString() ?? ''; + } + + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->group->setUuid($proto); + + return $this; + } + + public function getName(): string + { + return $this->group->getName(); + } + + public function setName(string $name): self + { + $this->group->setName($name); + + return $this; + } + + /** + * @return array{r: float, g: float, b: float, a: float}|null + */ + public function getColor(): ?array + { + if (!$this->group->hasColor()) { + return null; + } + + $color = $this->group->getColor(); + + return [ + 'r' => $color->getRed(), + 'g' => $color->getGreen(), + 'b' => $color->getBlue(), + 'a' => $color->getAlpha(), + ]; + } + + public function getColorHex(): ?string + { + $color = $this->getColor(); + if ($color === null) { + return null; + } + + return sprintf( + '#%02X%02X%02X', + (int) round(max(0.0, min(1.0, $color['r'])) * 255), + (int) round(max(0.0, min(1.0, $color['g'])) * 255), + (int) round(max(0.0, min(1.0, $color['b'])) * 255), + ); + } + + /** + * @param array{r: float, g: float, b: float, a?: float}|null $color + */ + public function setColor(?array $color): self + { + if ($color === null) { + $this->group->clearColor(); + + return $this; + } + + $proto = new Color(); + $proto->setRed((float) $color['r']); + $proto->setGreen((float) $color['g']); + $proto->setBlue((float) $color['b']); + $proto->setAlpha((float) ($color['a'] ?? 1.0)); + $this->group->setColor($proto); + + return $this; + } + + public function getHotKey(): ?HotKey + { + return $this->group->getHotKey(); + } + + public function getApplicationGroupName(): string + { + return $this->group->getApplicationGroupName(); + } + + public function getApplicationGroupUuid(): string + { + return $this->group->getApplicationGroupIdentifier()?->getString() ?? ''; + } + + public function getProto(): GroupProto + { + return $this->group; + } +} diff --git a/src/GroupLibrary.php b/src/GroupLibrary.php new file mode 100644 index 0000000..01543d1 --- /dev/null +++ b/src/GroupLibrary.php @@ -0,0 +1,117 @@ + */ + private array $groupsByUuid = []; + + /** @var array */ + private array $groupsByName = []; + + public function __construct( + private readonly ProGroupsDocument $document, + ) { + $this->rebuildIndex(); + } + + /** + * @return GroupDefinition[] + */ + public function getGroups(): array + { + return $this->groups; + } + + public function count(): int + { + return count($this->groups); + } + + public function getGroupByUuid(string $uuid): ?GroupDefinition + { + return $this->groupsByUuid[strtoupper($uuid)] ?? null; + } + + public function getGroupByName(string $name): ?GroupDefinition + { + return $this->groupsByName[$name] ?? null; + } + + public function addGroup(string $name, string $uuid): GroupDefinition + { + $proto = new GroupProto(); + $uuidProto = new UUID(); + $uuidProto->setString($uuid); + $proto->setUuid($uuidProto); + $proto->setName($name); + + $existing = iterator_to_array($this->document->getGroups()); + $existing[] = $proto; + $this->document->setGroups($existing); + $this->rebuildIndex(); + + return $this->getGroupByUuid($uuid) ?? new GroupDefinition($proto); + } + + public function removeGroup(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getGroups() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setGroups($kept); + $this->rebuildIndex(); + + return true; + } + + public function getDocument(): ProGroupsDocument + { + return $this->document; + } + + private function rebuildIndex(): void + { + $this->groups = []; + $this->groupsByUuid = []; + $this->groupsByName = []; + + foreach ($this->document->getGroups() as $proto) { + $group = new GroupDefinition($proto); + $this->groups[] = $group; + + $uuid = strtoupper($group->getUuid()); + if ($uuid !== '') { + $this->groupsByUuid[$uuid] = $group; + } + + $name = $group->getName(); + if ($name !== '') { + $this->groupsByName[$name] ??= $group; + } + } + } +} diff --git a/src/GroupsFileReader.php b/src/GroupsFileReader.php new file mode 100644 index 0000000..e95d4ba --- /dev/null +++ b/src/GroupsFileReader.php @@ -0,0 +1,38 @@ +mergeFromString($data); + + return new GroupLibrary($document); + } +} diff --git a/src/GroupsFileWriter.php b/src/GroupsFileWriter.php new file mode 100644 index 0000000..482f6a7 --- /dev/null +++ b/src/GroupsFileWriter.php @@ -0,0 +1,26 @@ +getDocument()->serializeToString(); + $writtenBytes = file_put_contents($filePath, $data); + + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write Groups file: %s', $filePath)); + } + } +} diff --git a/src/KeyMapping.php b/src/KeyMapping.php new file mode 100644 index 0000000..e217f5b --- /dev/null +++ b/src/KeyMapping.php @@ -0,0 +1,82 @@ +mapping->getUuid()?->getString() ?? ''; + } + + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->mapping->setUuid($proto); + + return $this; + } + + public function getName(): string + { + return $this->mapping->getName(); + } + + public function setName(string $name): self + { + $this->mapping->setName($name); + + return $this; + } + + public function getHotKey(): ?HotKey + { + if (!$this->mapping->hasHotKey()) { + return null; + } + + return $this->mapping->getHotKey(); + } + + public function setHotKey(?HotKey $hotKey): self + { + if ($hotKey === null) { + $this->mapping->clearHotKey(); + + return $this; + } + + $this->mapping->setHotKey($hotKey); + + return $this; + } + + public function getTarget(): string + { + return $this->mapping->getTarget(); + } + + public function setTarget(string $target): self + { + $this->mapping->setTarget($target); + + return $this; + } + + public function getProto(): MappingProto + { + return $this->mapping; + } +} diff --git a/src/KeyMappingsFileReader.php b/src/KeyMappingsFileReader.php new file mode 100644 index 0000000..0038d39 --- /dev/null +++ b/src/KeyMappingsFileReader.php @@ -0,0 +1,38 @@ +mergeFromString($data); + + return new KeyMappingsLibrary($document); + } +} diff --git a/src/KeyMappingsFileWriter.php b/src/KeyMappingsFileWriter.php new file mode 100644 index 0000000..dae0778 --- /dev/null +++ b/src/KeyMappingsFileWriter.php @@ -0,0 +1,26 @@ +getDocument()->serializeToString(); + $writtenBytes = file_put_contents($filePath, $data); + + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write KeyMappings file: %s', $filePath)); + } + } +} diff --git a/src/KeyMappingsLibrary.php b/src/KeyMappingsLibrary.php new file mode 100644 index 0000000..fff4441 --- /dev/null +++ b/src/KeyMappingsLibrary.php @@ -0,0 +1,139 @@ + */ + private array $mappingsByUuid = []; + + /** @var array */ + private array $mappingsByName = []; + + public function __construct( + private readonly KeyMappingsDocument $document, + ) { + $this->rebuildIndex(); + } + + /** + * Return key mappings in document order. + * + * @return KeyMapping[] + */ + public function getMappings(): array + { + return $this->mappings; + } + + public function count(): int + { + return count($this->mappings); + } + + public function getMappingByUuid(string $uuid): ?KeyMapping + { + return $this->mappingsByUuid[strtoupper($uuid)] ?? null; + } + + public function getMappingByName(string $name): ?KeyMapping + { + return $this->mappingsByName[$name] ?? null; + } + + public function addMapping(string $name, string $uuid, string $target = ''): KeyMapping + { + $proto = new MappingProto(); + $uuidProto = new UUID(); + $uuidProto->setString($uuid); + $proto->setUuid($uuidProto); + $proto->setName($name); + $proto->setTarget($target); + + $existing = iterator_to_array($this->document->getMappings()); + $existing[] = $proto; + $this->document->setMappings($existing); + $this->rebuildIndex(); + + return $this->getMappingByUuid($uuid) ?? new KeyMapping($proto); + } + + public function removeMapping(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getMappings() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setMappings($kept); + $this->rebuildIndex(); + + return true; + } + + public function getApplicationInfo(): ?ApplicationInfo + { + return $this->document->getApplicationInfo(); + } + + public function setApplicationInfo(?ApplicationInfo $applicationInfo): self + { + if ($applicationInfo === null) { + $this->document->clearApplicationInfo(); + + return $this; + } + + $this->document->setApplicationInfo($applicationInfo); + + return $this; + } + + public function getDocument(): KeyMappingsDocument + { + return $this->document; + } + + private function rebuildIndex(): void + { + $this->mappings = []; + $this->mappingsByUuid = []; + $this->mappingsByName = []; + + foreach ($this->document->getMappings() as $proto) { + $mapping = new KeyMapping($proto); + $this->mappings[] = $mapping; + + $uuid = strtoupper($mapping->getUuid()); + if ($uuid !== '') { + $this->mappingsByUuid[$uuid] = $mapping; + } + + $name = $mapping->getName(); + if ($name !== '') { + $this->mappingsByName[$name] ??= $mapping; + } + } + } +} diff --git a/src/Label.php b/src/Label.php index 797e1fa..b673a5c 100644 --- a/src/Label.php +++ b/src/Label.php @@ -4,15 +4,10 @@ declare(strict_types=1); namespace ProPresenter\Parser; +use InvalidArgumentException; use Rv\Data\Action\Label as LabelProto; +use Rv\Data\Color; -/** - * Wraps a protobuf {@see LabelProto} from the global ProPresenter `Labels` - * file. Surfaces the label's display name (the protobuf `text` field) and an - * optional color used by the UI to tint slides / cues that carry the label. - * - * Labels do not have a UUID — identity is the name string. - */ class Label { public function __construct( @@ -20,30 +15,24 @@ class Label ) { } - /** - * Display name (the protobuf `text` field; this is what the - * ProPresenter UI renders next to the swatch). - */ public function getName(): string { return $this->label->getText(); } - /** - * Whether the label carries an explicit color message (the absence of a - * color is not the same as black: ProPresenter renders unset labels with - * the default UI color). - */ + public function setName(string $name): self + { + $this->label->setText($name); + + return $this; + } + public function hasColor(): bool { return $this->label->hasColor(); } /** - * Get the label's color as an associative array, or null when unset. - * - * Channels are floats in the 0..1 range as stored by ProPresenter. - * * @return array{r: float, g: float, b: float, a: float}|null */ public function getColor(): ?array @@ -62,10 +51,6 @@ class Label ]; } - /** - * Convenience: render the color as a 6-digit `#RRGGBB` hex string, - * dropping alpha. Returns null when the label has no color. - */ public function getColorHex(): ?string { $color = $this->getColor(); @@ -82,8 +67,48 @@ class Label } /** - * Get the underlying protobuf Label message. + * Set the label's color. Pass `null` to remove the color (the UI will + * fall back to its default tint). + * + * @param array{r: float, g: float, b: float, a?: float}|null $color */ + public function setColor(?array $color): self + { + if ($color === null) { + $this->label->clearColor(); + + return $this; + } + + $proto = new Color(); + $proto->setRed((float) $color['r']); + $proto->setGreen((float) $color['g']); + $proto->setBlue((float) $color['b']); + $proto->setAlpha((float) ($color['a'] ?? 1.0)); + $this->label->setColor($proto); + + return $this; + } + + /** + * Convenience setter: accepts a `#RRGGBB` or `#RRGGBBAA` hex value and + * applies it to the label. Alpha defaults to 1.0 when missing. + */ + public function setColorHex(string $hex): self + { + $hex = ltrim($hex, '#'); + if (!preg_match('/^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/', $hex)) { + throw new InvalidArgumentException(sprintf('Invalid hex color: %s', $hex)); + } + + $r = hexdec(substr($hex, 0, 2)) / 255.0; + $g = hexdec(substr($hex, 2, 2)) / 255.0; + $b = hexdec(substr($hex, 4, 2)) / 255.0; + $a = strlen($hex) === 8 ? hexdec(substr($hex, 6, 2)) / 255.0 : 1.0; + + return $this->setColor(['r' => $r, 'g' => $g, 'b' => $b, 'a' => $a]); + } + public function getProto(): LabelProto { return $this->label; diff --git a/src/LabelLibrary.php b/src/LabelLibrary.php index f13c89a..ce6f4e1 100644 --- a/src/LabelLibrary.php +++ b/src/LabelLibrary.php @@ -4,17 +4,10 @@ declare(strict_types=1); namespace ProPresenter\Parser; +use Rv\Data\Action\Label as LabelProto; +use Rv\Data\Color; use Rv\Data\ProLabelsDocument; -/** - * Wraps the protobuf {@see ProLabelsDocument} — the global ProPresenter - * `Labels` file which lists every named label and its UI color. - * - * Labels are identified by name only (there is no UUID field). Names are - * expected to be unique inside the document; if the source file violates - * that, the first occurrence wins for {@see getLabelByName()} but every - * label is preserved in {@see getLabels()} in document order. - */ class LabelLibrary { /** @var Label[] */ @@ -30,16 +23,7 @@ class LabelLibrary private readonly ProLabelsDocument $document, ) { foreach ($this->document->getLabels() as $labelProto) { - $label = new Label($labelProto); - $this->labels[] = $label; - - $name = $label->getName(); - if ($name === '') { - continue; - } - - $this->labelsByName[$name] ??= $label; - $this->labelsByNameLower[strtolower($name)] ??= $label; + $this->register(new Label($labelProto)); } } @@ -56,28 +40,95 @@ class LabelLibrary return count($this->labels); } - /** - * Exact-name lookup. Use {@see findLabelByName()} for a - * case-insensitive variant. - */ public function getLabelByName(string $name): ?Label { return $this->labelsByName[$name] ?? null; } - /** - * Case-insensitive name lookup. - */ public function findLabelByName(string $name): ?Label { return $this->labelsByNameLower[strtolower($name)] ?? null; } /** - * Get the underlying protobuf ProLabelsDocument. + * Append a brand-new label to the document. + * + * @param array{r: float, g: float, b: float, a?: float}|null $color */ + public function addLabel(string $name, ?array $color = null): Label + { + $proto = new LabelProto(); + $proto->setText($name); + if ($color !== null) { + $colorProto = new Color(); + $colorProto->setRed((float) $color['r']); + $colorProto->setGreen((float) $color['g']); + $colorProto->setBlue((float) $color['b']); + $colorProto->setAlpha((float) ($color['a'] ?? 1.0)); + $proto->setColor($colorProto); + } + + $existing = iterator_to_array($this->document->getLabels()); + $existing[] = $proto; + $this->document->setLabels($existing); + + $label = new Label($proto); + $this->register($label); + + return $label; + } + + /** + * Remove a label by its current name. Returns true when something was + * removed. + */ + public function removeLabel(string $name): bool + { + $kept = []; + $removed = false; + foreach ($this->document->getLabels() as $proto) { + if (!$removed && $proto->getText() === $name) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setLabels($kept); + $this->rebuildIndex(); + + return true; + } + public function getDocument(): ProLabelsDocument { return $this->document; } + + private function register(Label $label): void + { + $this->labels[] = $label; + + $name = $label->getName(); + if ($name === '') { + return; + } + + $this->labelsByName[$name] ??= $label; + $this->labelsByNameLower[strtolower($name)] ??= $label; + } + + private function rebuildIndex(): void + { + $this->labels = []; + $this->labelsByName = []; + $this->labelsByNameLower = []; + foreach ($this->document->getLabels() as $proto) { + $this->register(new Label($proto)); + } + } } diff --git a/src/LabelsFileWriter.php b/src/LabelsFileWriter.php new file mode 100644 index 0000000..90367a1 --- /dev/null +++ b/src/LabelsFileWriter.php @@ -0,0 +1,26 @@ +getDocument()->serializeToString(); + $writtenBytes = file_put_contents($filePath, $data); + + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write Labels file: %s', $filePath)); + } + } +} diff --git a/src/Macro.php b/src/Macro.php index cfc5182..e47bf93 100644 --- a/src/Macro.php +++ b/src/Macro.php @@ -4,16 +4,10 @@ declare(strict_types=1); namespace ProPresenter\Parser; +use Rv\Data\Color; use Rv\Data\MacrosDocument\Macro as MacroProto; +use Rv\Data\UUID; -/** - * Wraps a protobuf Macro, exposing its identifying fields (UUID, name) plus - * convenience metadata (color, action count, image type, startup flag). - * - * Macros live in the global ProPresenter `Macros` document and may belong to - * one or more {@see MacroCollection}s. Membership is resolved by - * {@see MacroLibrary}, not by this wrapper. - */ class Macro { public function __construct( @@ -21,25 +15,33 @@ class Macro ) { } - /** - * Get the macro's UUID as an upper-case string (empty when unset). - */ public function getUuid(): string { return $this->macro->getUuid()?->getString() ?? ''; } - /** - * Get the macro's display name. - */ + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->macro->setUuid($proto); + + return $this; + } + public function getName(): string { return $this->macro->getName(); } + public function setName(string $name): self + { + $this->macro->setName($name); + + return $this; + } + /** - * Get the macro's color as an associative array, or null when unset. - * * @return array{r: float, g: float, b: float, a: float}|null */ public function getColor(): ?array @@ -59,35 +61,71 @@ class Macro } /** - * Whether the macro is configured to fire on application startup. + * @param array{r: float, g: float, b: float, a?: float}|null $color */ + public function setColor(?array $color): self + { + if ($color === null) { + $this->macro->clearColor(); + + return $this; + } + + $proto = new Color(); + $proto->setRed((float) $color['r']); + $proto->setGreen((float) $color['g']); + $proto->setBlue((float) $color['b']); + $proto->setAlpha((float) ($color['a'] ?? 1.0)); + $this->macro->setColor($proto); + + return $this; + } + public function getTriggerOnStartup(): bool { return $this->macro->getTriggerOnStartup(); } - /** - * Number of action entries attached to this macro. - * - * Action payloads are not exposed by this wrapper; use {@see getProto()} to - * inspect them via the generated protobuf classes. - */ + public function setTriggerOnStartup(bool $value): self + { + $this->macro->setTriggerOnStartup($value); + + return $this; + } + public function getActionCount(): int { return count($this->macro->getActions()); } - /** - * Icon enum value (see {@see \Rv\Data\MacrosDocument\Macro\ImageType}). - */ public function getImageType(): int { return $this->macro->getImageType(); } + public function setImageType(int $value): self + { + $this->macro->setImageType($value); + + return $this; + } + /** - * Get the underlying protobuf Macro message. + * Custom icon bytes (PNG/JPG). Empty when ProPresenter uses one of the + * built-in `image_type` icons. */ + public function getImageData(): string + { + return $this->macro->getImageData(); + } + + public function setImageData(string $bytes): self + { + $this->macro->setImageData($bytes); + + return $this; + } + public function getProto(): MacroProto { return $this->macro; diff --git a/src/MacroCollection.php b/src/MacroCollection.php index 6f8a42c..7fe1ba1 100644 --- a/src/MacroCollection.php +++ b/src/MacroCollection.php @@ -5,11 +5,9 @@ declare(strict_types=1); namespace ProPresenter\Parser; use Rv\Data\MacrosDocument\MacroCollection as MacroCollectionProto; +use Rv\Data\MacrosDocument\MacroCollection\Item as ItemProto; +use Rv\Data\UUID; -/** - * Wraps a protobuf MacroCollection. A collection groups macros by UUID - * reference; macro definitions themselves live at the document root. - */ class MacroCollection { public function __construct( @@ -17,28 +15,33 @@ class MacroCollection ) { } - /** - * Get the collection's UUID as a string (empty when unset). - */ public function getUuid(): string { return $this->collection->getUuid()?->getString() ?? ''; } - /** - * Get the collection's display name (e.g. "Ablauf"). - */ + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->collection->setUuid($proto); + + return $this; + } + public function getName(): string { return $this->collection->getName(); } + public function setName(string $name): self + { + $this->collection->setName($name); + + return $this; + } + /** - * Get the UUIDs of macros referenced by this collection, in order. - * - * Items in the protobuf use a `oneof` ItemType — currently only - * `macro_id` is defined. Items without a populated reference are skipped. - * * @return string[] */ public function getMacroUuids(): array @@ -55,8 +58,39 @@ class MacroCollection } /** - * Get the underlying protobuf MacroCollection message. + * Replace the collection's referenced macro UUIDs in one call. Pass UUID + * strings exactly as ProPresenter writes them (upper-case is conventional). + * + * @param string[] $uuids */ + public function setMacroUuids(array $uuids): self + { + $items = []; + foreach ($uuids as $uuid) { + $item = new ItemProto(); + $ref = new UUID(); + $ref->setString($uuid); + $item->setMacroId($ref); + $items[] = $item; + } + $this->collection->setItems($items); + + return $this; + } + + public function addMacroUuid(string $uuid): self + { + $items = iterator_to_array($this->collection->getItems()); + $item = new ItemProto(); + $ref = new UUID(); + $ref->setString($uuid); + $item->setMacroId($ref); + $items[] = $item; + $this->collection->setItems($items); + + return $this; + } + public function getProto(): MacroCollectionProto { return $this->collection; diff --git a/src/MacroLibrary.php b/src/MacroLibrary.php index e9372d6..0d927d2 100644 --- a/src/MacroLibrary.php +++ b/src/MacroLibrary.php @@ -5,16 +5,10 @@ declare(strict_types=1); namespace ProPresenter\Parser; use Rv\Data\MacrosDocument; +use Rv\Data\MacrosDocument\Macro as MacroProto; +use Rv\Data\MacrosDocument\MacroCollection as MacroCollectionProto; +use Rv\Data\UUID; -/** - * Wraps a protobuf MacrosDocument — the global ProPresenter `Macros` file - * which lists every macro definition and the collections that group them. - * - * Lookup helpers index macros and collections by UUID (case-insensitive) and - * by name, mirroring the convention used by {@see Song}. Collection - * membership is resolved by mapping {@see MacroCollection::getMacroUuids()} - * back through this library. - */ class MacroLibrary { /** @var Macro[] */ @@ -41,43 +35,7 @@ class MacroLibrary public function __construct( private readonly MacrosDocument $document, ) { - foreach ($this->document->getMacros() as $macroProto) { - $macro = new Macro($macroProto); - $this->macros[] = $macro; - - $uuid = strtoupper($macro->getUuid()); - if ($uuid !== '') { - $this->macrosByUuid[$uuid] = $macro; - } - - $name = $macro->getName(); - if ($name !== '') { - $this->macrosByName[$name] = $macro; - } - } - - foreach ($this->document->getMacroCollections() as $collectionProto) { - $collection = new MacroCollection($collectionProto); - $this->collections[] = $collection; - - $uuid = strtoupper($collection->getUuid()); - if ($uuid !== '') { - $this->collectionsByUuid[$uuid] = $collection; - } - - $name = $collection->getName(); - if ($name !== '') { - $this->collectionsByName[$name] = $collection; - } - - foreach ($collection->getMacroUuids() as $macroUuid) { - $key = strtoupper($macroUuid); - if ($key === '') { - continue; - } - $this->collectionsByMacroUuid[$key][] = $collection; - } - } + $this->rebuildIndex(); } /** @@ -117,10 +75,6 @@ class MacroLibrary } /** - * Resolve a collection's referenced macros to {@see Macro} wrappers. - * - * Unknown UUIDs (referenced but not defined at document root) are skipped. - * * @return Macro[] */ public function getMacrosForCollection(MacroCollection $collection): array @@ -137,10 +91,6 @@ class MacroLibrary } /** - * Get the collections a macro belongs to (membership is by UUID - * reference). A macro may legally appear in zero, one, or many - * collections. - * * @return MacroCollection[] */ public function getCollectionsForMacro(Macro $macro): array @@ -154,10 +104,141 @@ class MacroLibrary } /** - * Get the underlying protobuf MacrosDocument. + * Append a brand-new macro to the document. */ + public function addMacro(string $name, string $uuid): Macro + { + $proto = new MacroProto(); + $uuidProto = new UUID(); + $uuidProto->setString($uuid); + $proto->setUuid($uuidProto); + $proto->setName($name); + + $existing = iterator_to_array($this->document->getMacros()); + $existing[] = $proto; + $this->document->setMacros($existing); + + $this->rebuildIndex(); + + return $this->getMacroByUuid($uuid) ?? new Macro($proto); + } + + public function removeMacro(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getMacros() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setMacros($kept); + $this->rebuildIndex(); + + return true; + } + + public function addCollection(string $name, string $uuid): MacroCollection + { + $proto = new MacroCollectionProto(); + $uuidProto = new UUID(); + $uuidProto->setString($uuid); + $proto->setUuid($uuidProto); + $proto->setName($name); + + $existing = iterator_to_array($this->document->getMacroCollections()); + $existing[] = $proto; + $this->document->setMacroCollections($existing); + + $this->rebuildIndex(); + + return $this->getCollectionByUuid($uuid) ?? new MacroCollection($proto); + } + + public function removeCollection(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getMacroCollections() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setMacroCollections($kept); + $this->rebuildIndex(); + + return true; + } + public function getDocument(): MacrosDocument { return $this->document; } + + private function rebuildIndex(): void + { + $this->macros = []; + $this->collections = []; + $this->macrosByUuid = []; + $this->macrosByName = []; + $this->collectionsByUuid = []; + $this->collectionsByName = []; + $this->collectionsByMacroUuid = []; + + foreach ($this->document->getMacros() as $macroProto) { + $macro = new Macro($macroProto); + $this->macros[] = $macro; + + $uuid = strtoupper($macro->getUuid()); + if ($uuid !== '') { + $this->macrosByUuid[$uuid] = $macro; + } + + $name = $macro->getName(); + if ($name !== '') { + $this->macrosByName[$name] = $macro; + } + } + + foreach ($this->document->getMacroCollections() as $collectionProto) { + $collection = new MacroCollection($collectionProto); + $this->collections[] = $collection; + + $uuid = strtoupper($collection->getUuid()); + if ($uuid !== '') { + $this->collectionsByUuid[$uuid] = $collection; + } + + $name = $collection->getName(); + if ($name !== '') { + $this->collectionsByName[$name] = $collection; + } + + foreach ($collection->getMacroUuids() as $macroUuid) { + $key = strtoupper($macroUuid); + if ($key === '') { + continue; + } + $this->collectionsByMacroUuid[$key][] = $collection; + } + } + } } diff --git a/src/MacrosFileWriter.php b/src/MacrosFileWriter.php new file mode 100644 index 0000000..d56758d --- /dev/null +++ b/src/MacrosFileWriter.php @@ -0,0 +1,26 @@ +getDocument()->serializeToString(); + $writtenBytes = file_put_contents($filePath, $data); + + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write Macros file: %s', $filePath)); + } + } +} diff --git a/src/Message.php b/src/Message.php new file mode 100644 index 0000000..92e32e2 --- /dev/null +++ b/src/Message.php @@ -0,0 +1,103 @@ +message->getUuid()?->getString() ?? ''; + } + + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->message->setUuid($proto); + + return $this; + } + + public function getTitle(): string + { + return $this->message->getTitle(); + } + + public function setTitle(string $title): self + { + $this->message->setTitle($title); + + return $this; + } + + public function getTimeToRemove(): float + { + return $this->message->getTimeToRemove(); + } + + public function setTimeToRemove(float $timeToRemove): self + { + $this->message->setTimeToRemove($timeToRemove); + + return $this; + } + + public function isVisibleOnNetwork(): bool + { + return $this->message->getVisibleOnNetwork(); + } + + public function setVisibleOnNetwork(bool $visibleOnNetwork): self + { + $this->message->setVisibleOnNetwork($visibleOnNetwork); + + return $this; + } + + public function getMessageText(): string + { + return $this->message->getMessageText(); + } + + public function setMessageText(string $messageText): self + { + $this->message->setMessageText($messageText); + + return $this; + } + + public function getClearType(): int + { + return $this->message->getClearType(); + } + + public function setClearType(int $clearType): self + { + $this->message->setClearType($clearType); + + return $this; + } + + /** + * @return mixed raw repeated \Rv\Data\Message\Token protos + */ + public function getTokens(): mixed + { + return $this->message->getTokens(); + } + + public function getProto(): MessageProto + { + return $this->message; + } +} diff --git a/src/MessageLibrary.php b/src/MessageLibrary.php new file mode 100644 index 0000000..e7f8b3e --- /dev/null +++ b/src/MessageLibrary.php @@ -0,0 +1,123 @@ + */ + private array $messagesByUuid = []; + + /** @var array */ + private array $messagesByName = []; + + public function __construct( + private readonly MessageDocument $document, + ) { + $this->rebuildIndex(); + } + + /** + * @return Message[] + */ + public function getMessages(): array + { + return $this->messages; + } + + public function count(): int + { + return count($this->messages); + } + + public function getMessageByUuid(string $uuid): ?Message + { + return $this->messagesByUuid[strtoupper($uuid)] ?? null; + } + + public function getMessageByName(string $name): ?Message + { + return $this->messagesByName[$name] ?? null; + } + + public function addMessage(string $title, string $uuid): Message + { + $proto = new MessageProto(); + $uuidProto = new UUID(); + $uuidProto->setString($uuid); + $proto->setUuid($uuidProto); + $proto->setTitle($title); + + $existing = iterator_to_array($this->document->getMessages()); + $existing[] = $proto; + $this->document->setMessages($existing); + $this->rebuildIndex(); + + return $this->getMessageByUuid($uuid) ?? new Message($proto); + } + + public function removeMessage(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getMessages() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setMessages($kept); + $this->rebuildIndex(); + + return true; + } + + public function getApplicationInfo(): ?ApplicationInfo + { + return $this->document->getApplicationInfo(); + } + + public function getDocument(): MessageDocument + { + return $this->document; + } + + private function rebuildIndex(): void + { + $this->messages = []; + $this->messagesByUuid = []; + $this->messagesByName = []; + + foreach ($this->document->getMessages() as $proto) { + $message = new Message($proto); + $this->messages[] = $message; + + $uuid = strtoupper($message->getUuid()); + if ($uuid !== '') { + $this->messagesByUuid[$uuid] = $message; + } + + $title = $message->getTitle(); + if ($title !== '') { + $this->messagesByName[$title] ??= $message; + } + } + } +} diff --git a/src/MessagesFileReader.php b/src/MessagesFileReader.php new file mode 100644 index 0000000..c5524f1 --- /dev/null +++ b/src/MessagesFileReader.php @@ -0,0 +1,38 @@ +mergeFromString($data); + + return new MessageLibrary($document); + } +} diff --git a/src/MessagesFileWriter.php b/src/MessagesFileWriter.php new file mode 100644 index 0000000..b664826 --- /dev/null +++ b/src/MessagesFileWriter.php @@ -0,0 +1,24 @@ +getDocument()->serializeToString()); + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write Messages file: %s', $filePath)); + } + } +} diff --git a/src/Prop.php b/src/Prop.php new file mode 100644 index 0000000..f23fd5c --- /dev/null +++ b/src/Prop.php @@ -0,0 +1,77 @@ +cue->getUuid()?->getString() ?? ''; + } + + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->cue->setUuid($proto); + + return $this; + } + + public function getName(): string + { + return $this->cue->getName(); + } + + public function setName(string $name): self + { + $this->cue->setName($name); + + return $this; + } + + public function isEnabled(): bool + { + return $this->cue->getIsEnabled(); + } + + public function setEnabled(bool $enabled): self + { + $this->cue->setIsEnabled($enabled); + + return $this; + } + + public function getCompletionTime(): float + { + return $this->cue->getCompletionTime(); + } + + public function setCompletionTime(float $completionTime): self + { + $this->cue->setCompletionTime($completionTime); + + return $this; + } + + public function getActions(): RepeatedField + { + return $this->cue->getActions(); + } + + public function getProto(): Cue + { + return $this->cue; + } +} diff --git a/src/PropLibrary.php b/src/PropLibrary.php new file mode 100644 index 0000000..3f4dc45 --- /dev/null +++ b/src/PropLibrary.php @@ -0,0 +1,115 @@ + */ + private array $propsByUuid = []; + + /** @var array */ + private array $propsByName = []; + + public function __construct( + private readonly PropDocument $document, + ) { + $this->rebuildIndex(); + } + + public function getDocument(): PropDocument + { + return $this->document; + } + + /** @return Prop[] */ + public function getProps(): array + { + return $this->props; + } + + public function getPropByUuid(string $uuid): ?Prop + { + return $this->propsByUuid[strtoupper($uuid)] ?? null; + } + + public function getPropByName(string $name): ?Prop + { + return $this->propsByName[$name] ?? null; + } + + public function addProp(Prop|Cue $prop): Prop + { + $proto = $prop instanceof Prop ? $prop->getProto() : $prop; + $existing = iterator_to_array($this->document->getCues()); + $existing[] = $proto; + $this->document->setCues($existing); + $this->rebuildIndex(); + + return $this->props[array_key_last($this->props)]; + } + + public function removeProp(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getCues() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setCues($kept); + $this->rebuildIndex(); + + return true; + } + + public function count(): int + { + return count($this->props); + } + + public function getApplicationInfo(): ?ApplicationInfo + { + return $this->document->getApplicationInfo(); + } + + private function rebuildIndex(): void + { + $this->props = []; + $this->propsByUuid = []; + $this->propsByName = []; + + foreach ($this->document->getCues() as $proto) { + $prop = new Prop($proto); + $this->props[] = $prop; + + $uuid = strtoupper($prop->getUuid()); + if ($uuid !== '') { + $this->propsByUuid[$uuid] = $prop; + } + + $name = $prop->getName(); + if ($name !== '') { + $this->propsByName[$name] ??= $prop; + } + } + } +} diff --git a/src/PropsFileReader.php b/src/PropsFileReader.php new file mode 100644 index 0000000..1e6a62a --- /dev/null +++ b/src/PropsFileReader.php @@ -0,0 +1,35 @@ +mergeFromString($data); + + return new PropLibrary($document); + } +} diff --git a/src/PropsFileWriter.php b/src/PropsFileWriter.php new file mode 100644 index 0000000..c6b180a --- /dev/null +++ b/src/PropsFileWriter.php @@ -0,0 +1,22 @@ +getDocument()->serializeToString()) === false) { + throw new RuntimeException(sprintf('Unable to write Props file: %s', $filePath)); + } + } +} diff --git a/src/Screen.php b/src/Screen.php new file mode 100644 index 0000000..37d6bc8 --- /dev/null +++ b/src/Screen.php @@ -0,0 +1,71 @@ +screen->getName(); + } + + public function setName(string $name): self + { + $this->screen->setName($name); + + return $this; + } + + public function getUuid(): string + { + return $this->screen->getUuid()?->getString() ?? ''; + } + + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->screen->setUuid($proto); + + return $this; + } + + public function getScreenType(): int + { + return $this->screen->getScreenType(); + } + + public function setScreenType(int $screenType): self + { + $this->screen->setScreenType($screenType); + + return $this; + } + + public function getIndex(): ?int + { + if ($this->screen->hasArrangementSingle()) { + $arrangement = $this->screen->getArrangementSingle(); + if ($arrangement !== null && count($arrangement->getScreens()) > 0) { + return 0; + } + } + + return null; + } + + public function getProto(): ProPresenterScreen + { + return $this->screen; + } +} diff --git a/src/StageFileReader.php b/src/StageFileReader.php new file mode 100644 index 0000000..25c447e --- /dev/null +++ b/src/StageFileReader.php @@ -0,0 +1,38 @@ +mergeFromString($data); + + return new StageLibrary($document); + } +} diff --git a/src/StageFileWriter.php b/src/StageFileWriter.php new file mode 100644 index 0000000..7597927 --- /dev/null +++ b/src/StageFileWriter.php @@ -0,0 +1,24 @@ +getDocument()->serializeToString()); + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write Stage file: %s', $filePath)); + } + } +} diff --git a/src/StageLayout.php b/src/StageLayout.php new file mode 100644 index 0000000..59d6dce --- /dev/null +++ b/src/StageLayout.php @@ -0,0 +1,53 @@ +layout->getUuid()?->getString() ?? ''; + } + + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->layout->setUuid($proto); + + return $this; + } + + public function getName(): string + { + return $this->layout->getName(); + } + + public function setName(string $name): self + { + $this->layout->setName($name); + + return $this; + } + + public function getSlide(): ?Slide + { + return $this->layout->getSlide(); + } + + public function getProto(): LayoutProto + { + return $this->layout; + } +} diff --git a/src/StageLibrary.php b/src/StageLibrary.php new file mode 100644 index 0000000..fc722d6 --- /dev/null +++ b/src/StageLibrary.php @@ -0,0 +1,115 @@ + */ + private array $layoutsByUuid = []; + + /** @var array */ + private array $layoutsByName = []; + + public function __construct( + private readonly Document $document, + ) { + $this->rebuildIndex(); + } + + public function getDocument(): Document + { + return $this->document; + } + + /** @return StageLayout[] */ + public function getLayouts(): array + { + return $this->layouts; + } + + public function getLayoutByUuid(string $uuid): ?StageLayout + { + return $this->layoutsByUuid[strtoupper($uuid)] ?? null; + } + + public function getLayoutByName(string $name): ?StageLayout + { + return $this->layoutsByName[$name] ?? null; + } + + public function addLayout(StageLayout|LayoutProto $layout): StageLayout + { + $proto = $layout instanceof StageLayout ? $layout->getProto() : $layout; + $existing = iterator_to_array($this->document->getLayouts()); + $existing[] = $proto; + $this->document->setLayouts($existing); + $this->rebuildIndex(); + + return $this->layouts[array_key_last($this->layouts)]; + } + + public function removeLayout(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getLayouts() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setLayouts($kept); + $this->rebuildIndex(); + + return true; + } + + public function count(): int + { + return count($this->layouts); + } + + public function getApplicationInfo(): ?ApplicationInfo + { + return $this->document->getApplicationInfo(); + } + + private function rebuildIndex(): void + { + $this->layouts = []; + $this->layoutsByUuid = []; + $this->layoutsByName = []; + + foreach ($this->document->getLayouts() as $proto) { + $layout = new StageLayout($proto); + $this->layouts[] = $layout; + + $uuid = strtoupper($layout->getUuid()); + if ($uuid !== '') { + $this->layoutsByUuid[$uuid] = $layout; + } + + $name = $layout->getName(); + if ($name !== '') { + $this->layoutsByName[$name] ??= $layout; + } + } + } +} diff --git a/src/TestPatternsFileReader.php b/src/TestPatternsFileReader.php new file mode 100644 index 0000000..53fb0a5 --- /dev/null +++ b/src/TestPatternsFileReader.php @@ -0,0 +1,38 @@ +mergeFromString($data); + + return new TestPatternsLibrary($document); + } +} diff --git a/src/TestPatternsFileWriter.php b/src/TestPatternsFileWriter.php new file mode 100644 index 0000000..1d846a1 --- /dev/null +++ b/src/TestPatternsFileWriter.php @@ -0,0 +1,26 @@ +getDocument()->serializeToString(); + $writtenBytes = file_put_contents($filePath, $data); + + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write TestPatterns file: %s', $filePath)); + } + } +} diff --git a/src/TestPatternsLibrary.php b/src/TestPatternsLibrary.php new file mode 100644 index 0000000..33a4ffe --- /dev/null +++ b/src/TestPatternsLibrary.php @@ -0,0 +1,161 @@ + */ + private array $patternsByUuid = []; + + /** @var array */ + private array $patternsByName = []; + + public function __construct( + private readonly TestPatternDocument $document, + ) { + $this->rebuildIndex(); + } + + public function getState(): ?TestPatternStateData + { + if (!$this->document->hasState()) { + return null; + } + + return $this->document->getState(); + } + + public function setState(?TestPatternStateData $state): self + { + if ($state === null) { + $this->document->clearState(); + + return $this; + } + + $this->document->setState($state); + + return $this; + } + + public function getSelectedPatternUuid(): string + { + return $this->getState()?->getTestPatternId()?->getString() ?? ''; + } + + public function getSelectedPatternNameLocalizationKey(): string + { + return $this->getState()?->getTestPatternNameLocalizationKey() ?? ''; + } + + public function getDisplayLocation(): int + { + return $this->getState()?->getDisplayLocation() ?? 0; + } + + public function getSpecificScreenUuid(): string + { + return $this->getState()?->getSpecificScreen()?->getString() ?? ''; + } + + /** + * Return saved test pattern definitions in document order. + * + * @return TestPatternData[] + */ + public function getPatterns(): array + { + return $this->patterns; + } + + public function count(): int + { + return count($this->patterns); + } + + public function getPatternByUuid(string $uuid): ?TestPatternData + { + return $this->patternsByUuid[strtoupper($uuid)] ?? null; + } + + public function getPatternByName(string $nameLocalizationKey): ?TestPatternData + { + return $this->patternsByName[$nameLocalizationKey] ?? null; + } + + public function addPattern(string $nameLocalizationKey, string $uuid): TestPatternData + { + $proto = new TestPatternData(); + $uuidProto = new UUID(); + $uuidProto->setString($uuid); + $proto->setUuid($uuidProto); + $proto->setNameLocalizationKey($nameLocalizationKey); + + $existing = iterator_to_array($this->document->getPatterns()); + $existing[] = $proto; + $this->document->setPatterns($existing); + $this->rebuildIndex(); + + return $this->getPatternByUuid($uuid) ?? $proto; + } + + public function removePattern(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getPatterns() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setPatterns($kept); + $this->rebuildIndex(); + + return true; + } + + public function getDocument(): TestPatternDocument + { + return $this->document; + } + + private function rebuildIndex(): void + { + $this->patterns = []; + $this->patternsByUuid = []; + $this->patternsByName = []; + + foreach ($this->document->getPatterns() as $proto) { + $this->patterns[] = $proto; + + $uuid = strtoupper($proto->getUuid()?->getString() ?? ''); + if ($uuid !== '') { + $this->patternsByUuid[$uuid] = $proto; + } + + $name = $proto->getNameLocalizationKey(); + if ($name !== '') { + $this->patternsByName[$name] ??= $proto; + } + } + } +} diff --git a/src/ThemeAsset.php b/src/ThemeAsset.php new file mode 100644 index 0000000..e7af96c --- /dev/null +++ b/src/ThemeAsset.php @@ -0,0 +1,45 @@ +name; + } + + public function getBytes(): string + { + return $this->bytes; + } + + public function getSize(): int + { + return strlen($this->bytes); + } + + public function getMimeType(): string + { + return match (strtolower(pathinfo($this->name, PATHINFO_EXTENSION))) { + 'jpg', 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'svg' => 'image/svg+xml', + 'mp4' => 'video/mp4', + 'mov' => 'video/quicktime', + 'mp3' => 'audio/mpeg', + 'wav' => 'audio/wav', + default => 'application/octet-stream', + }; + } +} diff --git a/src/ThemeBundle.php b/src/ThemeBundle.php new file mode 100644 index 0000000..d95b9c0 --- /dev/null +++ b/src/ThemeBundle.php @@ -0,0 +1,135 @@ + */ + private array $slidesByName = []; + + /** @var array */ + private array $assetsByName = []; + + /** + * @param ThemeAsset[] $assets + */ + public function __construct( + private readonly Document $document, + array $assets = [], + ) { + foreach ($assets as $asset) { + $this->assetsByName[$asset->getName()] = $asset; + } + $this->rebuildSlideIndex(); + } + + public function getDocument(): Document + { + return $this->document; + } + + /** @return ThemeSlide[] */ + public function getSlides(): array + { + return $this->slides; + } + + public function getSlideByName(string $name): ?ThemeSlide + { + return $this->slidesByName[$name] ?? null; + } + + public function addSlide(ThemeSlide|TemplateSlide $slide): ThemeSlide + { + $proto = $slide instanceof ThemeSlide ? $slide->getProto() : $slide; + $existing = iterator_to_array($this->document->getSlides()); + $existing[] = $proto; + $this->document->setSlides($existing); + $this->rebuildSlideIndex(); + + return $this->slides[array_key_last($this->slides)]; + } + + public function removeSlide(string $name): bool + { + $kept = []; + $removed = false; + foreach ($this->document->getSlides() as $proto) { + if (!$removed && $proto->getName() === $name) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setSlides($kept); + $this->rebuildSlideIndex(); + + return true; + } + + /** @return ThemeAsset[] */ + public function getAssets(): array + { + return array_values($this->assetsByName); + } + + public function getAssetByName(string $name): ?ThemeAsset + { + return $this->assetsByName[$name] ?? null; + } + + public function addAsset(string $name, string $bytes): ThemeAsset + { + $asset = new ThemeAsset(basename($name), $bytes); + $this->assetsByName[$asset->getName()] = $asset; + + return $asset; + } + + public function removeAsset(string $name): bool + { + if (!isset($this->assetsByName[$name])) { + return false; + } + + unset($this->assetsByName[$name]); + + return true; + } + + public function count(): int + { + return count($this->slides); + } + + public function getAssetCount(): int + { + return count($this->assetsByName); + } + + private function rebuildSlideIndex(): void + { + $this->slides = []; + $this->slidesByName = []; + foreach ($this->document->getSlides() as $proto) { + $slide = new ThemeSlide($proto); + $this->slides[] = $slide; + if ($slide->getName() !== '') { + $this->slidesByName[$slide->getName()] ??= $slide; + } + } + } +} diff --git a/src/ThemeFileReader.php b/src/ThemeFileReader.php new file mode 100644 index 0000000..01654de --- /dev/null +++ b/src/ThemeFileReader.php @@ -0,0 +1,68 @@ +mergeFromString($data); + + return new ThemeBundle($document, self::readAssets($folderPath)); + } + + /** @return ThemeAsset[] */ + private static function readAssets(string $folderPath): array + { + $assetsPath = rtrim($folderPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'Assets'; + if (!is_dir($assetsPath)) { + return []; + } + + $entries = scandir($assetsPath); + if ($entries === false) { + throw new RuntimeException(sprintf('Unable to scan Assets directory: %s', $assetsPath)); + } + + $assets = []; + foreach ($entries as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + $path = $assetsPath . DIRECTORY_SEPARATOR . $entry; + if (!is_file($path)) { + continue; + } + $bytes = file_get_contents($path); + if ($bytes === false) { + throw new RuntimeException(sprintf('Unable to read Theme asset: %s', $path)); + } + $assets[] = new ThemeAsset($entry, $bytes); + } + + usort($assets, static fn (ThemeAsset $a, ThemeAsset $b): int => strcmp($a->getName(), $b->getName())); + + return $assets; + } +} diff --git a/src/ThemeFileWriter.php b/src/ThemeFileWriter.php new file mode 100644 index 0000000..49d72ee --- /dev/null +++ b/src/ThemeFileWriter.php @@ -0,0 +1,51 @@ +getDocument()->serializeToString()) === false) { + throw new RuntimeException(sprintf('Unable to write Theme file: %s', $themePath)); + } + + $assetsPath = rtrim($folderPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'Assets'; + if (!is_dir($assetsPath) && !mkdir($assetsPath, 0777, true)) { + throw new RuntimeException(sprintf('Unable to create Assets directory: %s', $assetsPath)); + } + + $expected = []; + foreach ($bundle->getAssets() as $asset) { + $name = basename($asset->getName()); + $expected[$name] = true; + $path = $assetsPath . DIRECTORY_SEPARATOR . $name; + if (file_put_contents($path, $asset->getBytes()) === false) { + throw new RuntimeException(sprintf('Unable to write Theme asset: %s', $path)); + } + } + + $entries = scandir($assetsPath); + if ($entries === false) { + throw new RuntimeException(sprintf('Unable to scan Assets directory: %s', $assetsPath)); + } + foreach ($entries as $entry) { + if ($entry === '.' || $entry === '..' || isset($expected[$entry])) { + continue; + } + $path = $assetsPath . DIRECTORY_SEPARATOR . $entry; + if (is_file($path) && !unlink($path)) { + throw new RuntimeException(sprintf('Unable to remove stale Theme asset: %s', $path)); + } + } + } +} diff --git a/src/ThemeSlide.php b/src/ThemeSlide.php new file mode 100644 index 0000000..e65b318 --- /dev/null +++ b/src/ThemeSlide.php @@ -0,0 +1,38 @@ +slide->getName(); + } + + public function setName(string $name): self + { + $this->slide->setName($name); + + return $this; + } + + public function getBaseSlide(): ?BaseSlide + { + return $this->slide->getBaseSlide(); + } + + public function getProto(): TemplateSlide + { + return $this->slide; + } +} diff --git a/src/Timer.php b/src/Timer.php new file mode 100644 index 0000000..603e919 --- /dev/null +++ b/src/Timer.php @@ -0,0 +1,78 @@ +timer->getUuid()?->getString() ?? ''; + } + + public function setUuid(string $uuid): self + { + $proto = new UUID(); + $proto->setString($uuid); + $this->timer->setUuid($proto); + + return $this; + } + + public function getName(): string + { + return $this->timer->getName(); + } + + public function setName(string $name): self + { + $this->timer->setName($name); + + return $this; + } + + public function getConfiguration(): ?Configuration + { + return $this->timer->getConfiguration(); + } + + public function isCountdown(): bool + { + return $this->timer->getConfiguration()?->hasCountdown() ?? false; + } + + public function isCountdownToTime(): bool + { + return $this->timer->getConfiguration()?->hasCountdownToTime() ?? false; + } + + public function isElapsedTime(): bool + { + return $this->timer->getConfiguration()?->hasElapsedTime() ?? false; + } + + public function getDurationSeconds(): ?int + { + $countdown = $this->timer->getConfiguration()?->getCountdown(); + if ($countdown === null) { + return null; + } + + return (int) round($countdown->getDuration()); + } + + public function getProto(): TimerProto + { + return $this->timer; + } +} diff --git a/src/TimersFileReader.php b/src/TimersFileReader.php new file mode 100644 index 0000000..fc05f76 --- /dev/null +++ b/src/TimersFileReader.php @@ -0,0 +1,37 @@ +mergeFromString($data); + + return new TimersLibrary($document); + } +} diff --git a/src/TimersFileWriter.php b/src/TimersFileWriter.php new file mode 100644 index 0000000..cdadc7e --- /dev/null +++ b/src/TimersFileWriter.php @@ -0,0 +1,24 @@ +getDocument()->serializeToString()); + if ($writtenBytes === false) { + throw new RuntimeException(sprintf('Unable to write Timers file: %s', $filePath)); + } + } +} diff --git a/src/TimersLibrary.php b/src/TimersLibrary.php new file mode 100644 index 0000000..8594b0e --- /dev/null +++ b/src/TimersLibrary.php @@ -0,0 +1,136 @@ + */ + private array $timersByUuid = []; + + /** @var array */ + private array $timersByName = []; + + public function __construct( + private readonly TimersDocument $document, + ) { + $this->rebuildIndex(); + } + + /** + * @return Timer[] + */ + public function getTimers(): array + { + return $this->timers; + } + + public function count(): int + { + return count($this->timers); + } + + public function getTimerByUuid(string $uuid): ?Timer + { + return $this->timersByUuid[strtoupper($uuid)] ?? null; + } + + public function getTimerByName(string $name): ?Timer + { + return $this->timersByName[$name] ?? null; + } + + public function addTimer(string $name, string $uuid): Timer + { + $proto = new TimerProto(); + $uuidProto = new UUID(); + $uuidProto->setString($uuid); + $proto->setUuid($uuidProto); + $proto->setName($name); + + $existing = iterator_to_array($this->document->getTimers()); + $existing[] = $proto; + $this->document->setTimers($existing); + $this->rebuildIndex(); + + return $this->getTimerByUuid($uuid) ?? new Timer($proto); + } + + public function removeTimer(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getTimers() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setTimers($kept); + $this->rebuildIndex(); + + return true; + } + + public function getClockFormat(): string + { + return $this->document->getClock()?->getFormat() ?? ''; + } + + public function setClockFormat(string $format): void + { + $clock = $this->document->getClock() ?? new Clock(); + $clock->setFormat($format); + $this->document->setClock($clock); + } + + public function getApplicationInfo(): ?ApplicationInfo + { + return $this->document->getApplicationInfo(); + } + + public function getDocument(): TimersDocument + { + return $this->document; + } + + private function rebuildIndex(): void + { + $this->timers = []; + $this->timersByUuid = []; + $this->timersByName = []; + + foreach ($this->document->getTimers() as $proto) { + $timer = new Timer($proto); + $this->timers[] = $timer; + + $uuid = strtoupper($timer->getUuid()); + if ($uuid !== '') { + $this->timersByUuid[$uuid] = $timer; + } + + $name = $timer->getName(); + if ($name !== '') { + $this->timersByName[$name] ??= $timer; + } + } + } +} diff --git a/src/WorkspaceFileReader.php b/src/WorkspaceFileReader.php new file mode 100644 index 0000000..971e837 --- /dev/null +++ b/src/WorkspaceFileReader.php @@ -0,0 +1,35 @@ +mergeFromString($data); + + return new WorkspaceLibrary($document); + } +} diff --git a/src/WorkspaceFileWriter.php b/src/WorkspaceFileWriter.php new file mode 100644 index 0000000..4973edf --- /dev/null +++ b/src/WorkspaceFileWriter.php @@ -0,0 +1,22 @@ +getDocument()->serializeToString()) === false) { + throw new RuntimeException(sprintf('Unable to write Workspace file: %s', $filePath)); + } + } +} diff --git a/src/WorkspaceLibrary.php b/src/WorkspaceLibrary.php new file mode 100644 index 0000000..3071481 --- /dev/null +++ b/src/WorkspaceLibrary.php @@ -0,0 +1,137 @@ + */ + private array $screensByUuid = []; + + /** @var array */ + private array $screensByName = []; + + public function __construct( + private readonly ProPresenterWorkspace $document, + ) { + $this->rebuildIndex(); + } + + public function getDocument(): ProPresenterWorkspace + { + return $this->document; + } + + /** @return Screen[] */ + public function getScreens(): array + { + return $this->screens; + } + + public function getScreenByName(string $name): ?Screen + { + return $this->screensByName[$name] ?? null; + } + + public function getScreenByUuid(string $uuid): ?Screen + { + return $this->screensByUuid[strtoupper($uuid)] ?? null; + } + + public function addScreen(Screen|ProPresenterScreen $screen): Screen + { + $proto = $screen instanceof Screen ? $screen->getProto() : $screen; + $existing = iterator_to_array($this->document->getProScreens()); + $existing[] = $proto; + $this->document->setProScreens($existing); + $this->rebuildIndex(); + + return $this->screens[array_key_last($this->screens)]; + } + + public function removeScreen(string $uuid): bool + { + $needle = strtoupper($uuid); + $kept = []; + $removed = false; + foreach ($this->document->getProScreens() as $proto) { + $current = strtoupper($proto->getUuid()?->getString() ?? ''); + if (!$removed && $current === $needle) { + $removed = true; + continue; + } + $kept[] = $proto; + } + + if (!$removed) { + return false; + } + + $this->document->setProScreens($kept); + $this->rebuildIndex(); + + return true; + } + + public function count(): int + { + return count($this->screens); + } + + public function getAudienceLooks(): RepeatedField + { + return $this->document->getAudienceLooks(); + } + + public function getMasks(): RepeatedField + { + return $this->document->getMasks(); + } + + public function getVideoInputs(): RepeatedField + { + return $this->document->getVideoInputs(); + } + + public function getSelectedLibraryName(): string + { + return $this->document->getSelectedLibraryName(); + } + + public function setSelectedLibraryName(string $name): self + { + $this->document->setSelectedLibraryName($name); + + return $this; + } + + private function rebuildIndex(): void + { + $this->screens = []; + $this->screensByUuid = []; + $this->screensByName = []; + + foreach ($this->document->getProScreens() as $proto) { + $screen = new Screen($proto); + $this->screens[] = $screen; + + $uuid = strtoupper($screen->getUuid()); + if ($uuid !== '') { + $this->screensByUuid[$uuid] = $screen; + } + + $name = $screen->getName(); + if ($name !== '') { + $this->screensByName[$name] ??= $screen; + } + } + } +} diff --git a/tests/CCLIFileReaderTest.php b/tests/CCLIFileReaderTest.php new file mode 100644 index 0000000..3d94ae6 --- /dev/null +++ b/tests/CCLIFileReaderTest.php @@ -0,0 +1,93 @@ +expectException(InvalidArgumentException::class); + CCLIFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-ccli'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = CCLIFileReader::read(self::REFERENCE_PATH); + + $this->assertInstanceOf(CCLILibrary::class, $library); + $this->assertSame(1, $library->count()); + } + + #[Test] + public function documentExposesLicenseAndDisplaySettings(): void + { + $library = CCLIFileReader::read(self::REFERENCE_PATH); + + $this->assertTrue($library->isCCLIDisplayEnabled()); + $this->assertSame('', $library->getCCLILicense()); + $this->assertSame(0, $library->getDisplayType()); + $this->assertNotNull($library->getTemplate()); + } + + #[Test] + public function settersUpdateDocument(): void + { + $library = CCLIFileReader::read(self::REFERENCE_PATH); + + $library->setCCLILicense('1234567'); + $library->setDisplayType(3); + $library->setCCLIDisplayEnabled(false); + + $this->assertSame('1234567', $library->getCCLILicense()); + $this->assertSame(3, $library->getDisplayType()); + $this->assertFalse($library->isCCLIDisplayEnabled()); + } + + #[Test] + public function addAndRemoveRoundTrip(): void + { + $library = CCLIFileReader::read(self::REFERENCE_PATH); + + $template = $library->getTemplate(); + $this->assertNotNull($template); + + $library->setTemplate(null); + $this->assertNull($library->getTemplate()); + + $library->setTemplate($template); + $this->assertNotNull($library->getTemplate()); + } + + #[Test] + public function writerProducesByteIdenticalRoundTrip(): void + { + $original = file_get_contents(self::REFERENCE_PATH); + $library = CCLIFileReader::read(self::REFERENCE_PATH); + + $tmp = tempnam(sys_get_temp_dir(), 'ccli_'); + try { + CCLIFileWriter::write($library, $tmp); + $roundTrip = CCLIFileReader::read($tmp); + $this->assertSame(strlen((string) $original), strlen((string) file_get_contents($tmp))); + $this->assertSame($library->isCCLIDisplayEnabled(), $roundTrip->isCCLIDisplayEnabled()); + $this->assertSame($library->getCCLILicense(), $roundTrip->getCCLILicense()); + $this->assertSame($library->getDisplayType(), $roundTrip->getDisplayType()); + $this->assertSame($library->getTemplate() !== null, $roundTrip->getTemplate() !== null); + } finally { + @unlink($tmp); + } + } +} diff --git a/tests/CalendarFileReaderTest.php b/tests/CalendarFileReaderTest.php new file mode 100644 index 0000000..c93e636 --- /dev/null +++ b/tests/CalendarFileReaderTest.php @@ -0,0 +1,91 @@ +expectException(InvalidArgumentException::class); + CalendarFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-calendar'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = CalendarFileReader::read(self::REFERENCE_PATH); + $this->assertInstanceOf(CalendarLibrary::class, $library); + $this->assertCount(3, $library->getEvents()); + $this->assertSame(3, $library->count()); + } + + #[Test] + public function eventsExposeNameAndUuid(): void + { + $first = CalendarFileReader::read(self::REFERENCE_PATH)->getEvents()[0]; + $this->assertInstanceOf(CalendarEvent::class, $first); + $this->assertSame('Doors Open', $first->getName()); + $this->assertSame('3E749EF4-0663-4F0F-AACA-BD801B6D8ACD', $first->getUuid()); + } + + #[Test] + public function eventsHaveExpectedNamesAndOpaqueBytes(): void + { + $events = CalendarFileReader::read(self::REFERENCE_PATH)->getEvents(); + $this->assertSame(['Doors Open', 'Godi Start', 'Doors Open'], array_map(static fn (CalendarEvent $event): string => $event->getName(), $events)); + foreach ($events as $event) { + $this->assertNotSame('', $event->getActionData()); + $this->assertNotSame('', $event->getMacroData()); + } + } + + #[Test] + public function lookupAndModeSucceed(): void + { + $library = CalendarFileReader::read(self::REFERENCE_PATH); + $event = $library->getEventByUuid('3e749ef4-0663-4f0f-aaca-bd801b6d8acd'); + $this->assertNotNull($event); + $this->assertSame('Doors Open', $event->getName()); + $this->assertSame(1, $library->getMode()); + $this->assertSame(1731833100, $event->getStartTimeSeconds()); + } + + #[Test] + public function addAndRemoveEventRoundTrip(): void + { + $library = CalendarFileReader::read(self::REFERENCE_PATH); + $library->addEvent('Test Event', '11111111-1111-1111-1111-111111111111'); + $this->assertSame(4, $library->count()); + $this->assertNotNull($library->getEventByUuid('11111111-1111-1111-1111-111111111111')); + $this->assertTrue($library->removeEvent('11111111-1111-1111-1111-111111111111')); + $this->assertSame(3, $library->count()); + } + + #[Test] + public function writerProducesByteIdenticalRoundTrip(): void + { + $first = tempnam(sys_get_temp_dir(), 'calendar_'); + $second = tempnam(sys_get_temp_dir(), 'calendar_'); + try { + CalendarFileWriter::write(CalendarFileReader::read(self::REFERENCE_PATH), $first); + CalendarFileWriter::write(CalendarFileReader::read($first), $second); + $this->assertSame(file_get_contents($first), file_get_contents($second)); + } finally { + @unlink($first ?: ''); + @unlink($second ?: ''); + } + } +} diff --git a/tests/ClearGroupsFileReaderTest.php b/tests/ClearGroupsFileReaderTest.php new file mode 100644 index 0000000..9761783 --- /dev/null +++ b/tests/ClearGroupsFileReaderTest.php @@ -0,0 +1,119 @@ +expectException(InvalidArgumentException::class); + ClearGroupsFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-clear-groups'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = ClearGroupsFileReader::read(self::REFERENCE_PATH); + + $this->assertInstanceOf(ClearGroupsLibrary::class, $library); + $this->assertCount(1, $library->getGroups()); + $this->assertSame(1, $library->count()); + } + + #[Test] + public function clearGroupsExposeNameAndUuid(): void + { + $library = ClearGroupsFileReader::read(self::REFERENCE_PATH); + $first = $library->getGroups()[0]; + + $this->assertInstanceOf(ClearGroupDefinition::class, $first); + $this->assertSame('Alles ausblenden', $first->getName()); + $this->assertSame('A91C6AFE-098F-4559-B2CF-D8373C589589', $first->getUuid()); + } + + #[Test] + public function lookupByUuidIsCaseInsensitive(): void + { + $library = ClearGroupsFileReader::read(self::REFERENCE_PATH); + + $upper = $library->getClearGroupByUuid('A91C6AFE-098F-4559-B2CF-D8373C589589'); + $lower = $library->getClearGroupByUuid('a91c6afe-098f-4559-b2cf-d8373c589589'); + + $this->assertNotNull($upper); + $this->assertSame($upper, $lower); + $this->assertSame('Alles ausblenden', $upper->getName()); + } + + #[Test] + public function lookupByNameSucceeds(): void + { + $library = ClearGroupsFileReader::read(self::REFERENCE_PATH); + + $group = $library->getClearGroupByName('Alles ausblenden'); + $this->assertNotNull($group); + $this->assertSame('A91C6AFE-098F-4559-B2CF-D8373C589589', $group->getUuid()); + } + + #[Test] + public function colorIsExposedAsHex(): void + { + $library = ClearGroupsFileReader::read(self::REFERENCE_PATH); + $group = $library->getClearGroupByName('Alles ausblenden'); + + $this->assertNotNull($group); + $this->assertSame('#FFFFFF', $group->getColorHex()); + $this->assertFalse($group->isIconTinted()); + } + + #[Test] + public function setColorHexUpdatesProto(): void + { + $library = ClearGroupsFileReader::read(self::REFERENCE_PATH); + $group = $library->getGroups()[0]; + + $group->setColor(['r' => 1.0, 'g' => 0.0, 'b' => 0.0]); + $this->assertSame('#FF0000', $group->getColorHex()); + } + + #[Test] + public function addAndRemoveClearGroupRoundTrip(): void + { + $library = ClearGroupsFileReader::read(self::REFERENCE_PATH); + + $library->addClearGroup('Test Clear', '11111111-1111-1111-1111-111111111111'); + $this->assertNotNull($library->getClearGroupByUuid('11111111-1111-1111-1111-111111111111')); + $this->assertSame(2, $library->count()); + + $this->assertTrue($library->removeClearGroup('11111111-1111-1111-1111-111111111111')); + $this->assertNull($library->getClearGroupByUuid('11111111-1111-1111-1111-111111111111')); + $this->assertSame(1, $library->count()); + } + + #[Test] + public function writerProducesByteIdenticalRoundTrip(): void + { + $original = file_get_contents(self::REFERENCE_PATH); + $library = ClearGroupsFileReader::read(self::REFERENCE_PATH); + + $tmp = tempnam(sys_get_temp_dir(), 'clear_groups_'); + try { + ClearGroupsFileWriter::write($library, $tmp); + $this->assertSame($original, file_get_contents($tmp)); + } finally { + @unlink($tmp); + } + } +} diff --git a/tests/CommunicationDevicesFileReaderTest.php b/tests/CommunicationDevicesFileReaderTest.php new file mode 100644 index 0000000..deeea81 --- /dev/null +++ b/tests/CommunicationDevicesFileReaderTest.php @@ -0,0 +1,89 @@ +expectException(InvalidArgumentException::class); + CommunicationDevicesFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-communication-devices'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = CommunicationDevicesFileReader::read(self::REFERENCE_PATH); + $this->assertInstanceOf(CommunicationDevicesLibrary::class, $library); + $this->assertCount(0, $library->getDevices()); + $this->assertSame(0, $library->count()); + } + + #[Test] + public function emptyListCanAddDevice(): void + { + $library = CommunicationDevicesFileReader::read(self::REFERENCE_PATH); + $device = (new CommunicationDevice()) + ->setId('device-1') + ->setName('Lighting Console') + ->setType('network') + ->setAddress('192.0.2.10'); + $library->addDevice($device); + $this->assertSame(1, $library->count()); + $this->assertSame('Lighting Console', $library->getDevices()[0]->getName()); + $this->assertSame('device-1', $library->getDevices()[0]->getId()); + } + + #[Test] + public function writeReadRoundTripPreservesDeviceSemantics(): void + { + $library = new CommunicationDevicesLibrary(); + $library->addDevice((new CommunicationDevice())->setId('device-1')->setName('Stage Router')->setType('tcp')->setAddress('10.0.0.5')); + $tmp = tempnam(sys_get_temp_dir(), 'devices_'); + try { + CommunicationDevicesFileWriter::write($library, $tmp); + $roundTrip = CommunicationDevicesFileReader::read($tmp); + $this->assertSame($library->getDocument(), $roundTrip->getDocument()); + } finally { + @unlink($tmp ?: ''); + } + } + + #[Test] + public function addAndRemoveDeviceRoundTrip(): void + { + $library = CommunicationDevicesFileReader::read(self::REFERENCE_PATH); + $library->addDevice((new CommunicationDevice())->setId('device-1')->setName('Stage Router')); + $this->assertSame(1, $library->count()); + $this->assertTrue($library->removeDevice('device-1')); + $this->assertSame(0, $library->count()); + } + + #[Test] + public function writerProducesByteIdenticalRoundTrip(): void + { + $first = tempnam(sys_get_temp_dir(), 'devices_'); + $second = tempnam(sys_get_temp_dir(), 'devices_'); + try { + CommunicationDevicesFileWriter::write(CommunicationDevicesFileReader::read(self::REFERENCE_PATH), $first); + CommunicationDevicesFileWriter::write(CommunicationDevicesFileReader::read($first), $second); + $this->assertSame(json_decode((string) file_get_contents($first), true), json_decode((string) file_get_contents($second), true)); + } finally { + @unlink($first ?: ''); + @unlink($second ?: ''); + } + } +} diff --git a/tests/GroupsFileReaderTest.php b/tests/GroupsFileReaderTest.php new file mode 100644 index 0000000..556e955 --- /dev/null +++ b/tests/GroupsFileReaderTest.php @@ -0,0 +1,124 @@ +expectException(InvalidArgumentException::class); + GroupsFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-groups'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = GroupsFileReader::read(self::REFERENCE_PATH); + + $this->assertInstanceOf(GroupLibrary::class, $library); + $this->assertCount(29, $library->getGroups()); + $this->assertSame(29, $library->count()); + } + + #[Test] + public function groupsExposeNameAndUuid(): void + { + $library = GroupsFileReader::read(self::REFERENCE_PATH); + $first = $library->getGroups()[0]; + + $this->assertInstanceOf(GroupDefinition::class, $first); + $this->assertSame('Vers', $first->getName()); + $this->assertSame('4E9D56A2-7E96-4975-97CC-44982257EF8A', $first->getUuid()); + } + + #[Test] + public function lookupByUuidIsCaseInsensitive(): void + { + $library = GroupsFileReader::read(self::REFERENCE_PATH); + + $upper = $library->getGroupByUuid('4E9D56A2-7E96-4975-97CC-44982257EF8A'); + $lower = $library->getGroupByUuid('4e9d56a2-7e96-4975-97cc-44982257ef8a'); + + $this->assertNotNull($upper); + $this->assertSame($upper, $lower); + $this->assertSame('Vers', $upper->getName()); + } + + #[Test] + public function lookupByNameSucceeds(): void + { + $library = GroupsFileReader::read(self::REFERENCE_PATH); + + $verse1 = $library->getGroupByName('Verse 1'); + $this->assertNotNull($verse1); + $this->assertSame('1D85C82C-EC82-44D8-8ED0-7742D46242C0', $verse1->getUuid()); + } + + #[Test] + public function colorIsExposedAsHex(): void + { + $library = GroupsFileReader::read(self::REFERENCE_PATH); + + $vers = $library->getGroupByName('Vers'); + $this->assertNotNull($vers); + $this->assertSame('#0077CC', $vers->getColorHex()); + + $color = $vers->getColor(); + $this->assertNotNull($color); + $this->assertEqualsWithDelta(1.0, $color['a'], 0.001); + } + + #[Test] + public function setColorHexUpdatesProto(): void + { + $library = GroupsFileReader::read(self::REFERENCE_PATH); + + $vers = $library->getGroupByName('Vers'); + $this->assertNotNull($vers); + + $vers->setColor(['r' => 1.0, 'g' => 0.0, 'b' => 0.0]); + $this->assertSame('#FF0000', $vers->getColorHex()); + } + + #[Test] + public function addAndRemoveGroupRoundTrip(): void + { + $library = GroupsFileReader::read(self::REFERENCE_PATH); + + $library->addGroup('Test Group', '11111111-1111-1111-1111-111111111111'); + $this->assertNotNull($library->getGroupByUuid('11111111-1111-1111-1111-111111111111')); + $this->assertSame(30, $library->count()); + + $this->assertTrue($library->removeGroup('11111111-1111-1111-1111-111111111111')); + $this->assertNull($library->getGroupByUuid('11111111-1111-1111-1111-111111111111')); + $this->assertSame(29, $library->count()); + } + + #[Test] + public function writerProducesByteIdenticalRoundTrip(): void + { + $original = file_get_contents(self::REFERENCE_PATH); + $library = GroupsFileReader::read(self::REFERENCE_PATH); + + $tmp = tempnam(sys_get_temp_dir(), 'groups_'); + try { + GroupsFileWriter::write($library, $tmp); + $this->assertSame($original, file_get_contents($tmp)); + } finally { + @unlink($tmp); + } + } +} diff --git a/tests/KeyMappingsFileReaderTest.php b/tests/KeyMappingsFileReaderTest.php new file mode 100644 index 0000000..88d6cee --- /dev/null +++ b/tests/KeyMappingsFileReaderTest.php @@ -0,0 +1,103 @@ +expectException(InvalidArgumentException::class); + KeyMappingsFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-key-mappings'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = KeyMappingsFileReader::read(self::REFERENCE_PATH); + + $this->assertInstanceOf(KeyMappingsLibrary::class, $library); + $this->assertCount(0, $library->getMappings()); + $this->assertSame(0, $library->count()); + $this->assertNotNull($library->getApplicationInfo()); + } + + #[Test] + public function mappingsExposeNameAndUuid(): void + { + $library = KeyMappingsFileReader::read(self::REFERENCE_PATH); + $mapping = $library->addMapping('Test Mapping', 'ABCDEFAB-1111-1111-1111-111111111111', 'target'); + + $this->assertInstanceOf(KeyMapping::class, $mapping); + $this->assertSame('Test Mapping', $mapping->getName()); + $this->assertSame('ABCDEFAB-1111-1111-1111-111111111111', $mapping->getUuid()); + $this->assertSame('target', $mapping->getTarget()); + } + + #[Test] + public function lookupByUuidIsCaseInsensitive(): void + { + $library = KeyMappingsFileReader::read(self::REFERENCE_PATH); + + $library->addMapping('Test Mapping', 'ABCDEFAB-1111-1111-1111-111111111111'); + $upper = $library->getMappingByUuid('ABCDEFAB-1111-1111-1111-111111111111'); + $lower = $library->getMappingByUuid('abcdefab-1111-1111-1111-111111111111'); + + $this->assertNotNull($upper); + $this->assertSame($upper, $lower); + $this->assertSame('Test Mapping', $upper->getName()); + } + + #[Test] + public function lookupByNameSucceeds(): void + { + $library = KeyMappingsFileReader::read(self::REFERENCE_PATH); + + $library->addMapping('Test Mapping', 'ABCDEFAB-1111-1111-1111-111111111111'); + $mapping = $library->getMappingByName('Test Mapping'); + + $this->assertNotNull($mapping); + $this->assertSame('ABCDEFAB-1111-1111-1111-111111111111', $mapping->getUuid()); + } + + #[Test] + public function addAndRemoveMappingRoundTrip(): void + { + $library = KeyMappingsFileReader::read(self::REFERENCE_PATH); + + $library->addMapping('Test Mapping', 'ABCDEFAB-1111-1111-1111-111111111111'); + $this->assertNotNull($library->getMappingByUuid('ABCDEFAB-1111-1111-1111-111111111111')); + $this->assertSame(1, $library->count()); + + $this->assertTrue($library->removeMapping('abcdefab-1111-1111-1111-111111111111')); + $this->assertNull($library->getMappingByUuid('ABCDEFAB-1111-1111-1111-111111111111')); + $this->assertSame(0, $library->count()); + } + + #[Test] + public function writerProducesByteIdenticalRoundTrip(): void + { + $original = file_get_contents(self::REFERENCE_PATH); + $library = KeyMappingsFileReader::read(self::REFERENCE_PATH); + + $tmp = tempnam(sys_get_temp_dir(), 'key_mappings_'); + try { + KeyMappingsFileWriter::write($library, $tmp); + $this->assertSame($original, file_get_contents($tmp)); + } finally { + @unlink($tmp); + } + } +} diff --git a/tests/LabelsFileWriterTest.php b/tests/LabelsFileWriterTest.php new file mode 100644 index 0000000..45b4466 --- /dev/null +++ b/tests/LabelsFileWriterTest.php @@ -0,0 +1,71 @@ +assertSame($original, file_get_contents($tmp)); + } finally { + @unlink($tmp); + } + } + + #[Test] + public function addLabelPersistsThroughWriteRead(): void + { + $library = LabelsFileReader::read(self::REFERENCE_PATH); + $library->addLabel('CustomLabel', ['r' => 0.5, 'g' => 0.5, 'b' => 0.5, 'a' => 1.0]); + + $tmp = tempnam(sys_get_temp_dir(), 'labels_'); + try { + LabelsFileWriter::write($library, $tmp); + + $reload = LabelsFileReader::read($tmp); + $custom = $reload->getLabelByName('CustomLabel'); + $this->assertNotNull($custom); + $this->assertSame('#808080', $custom->getColorHex()); + } finally { + @unlink($tmp); + } + } + + #[Test] + public function setColorHexAcceptsRrggbb(): void + { + $library = LabelsFileReader::read(self::REFERENCE_PATH); + $beamer = $library->getLabelByName('KeyVisual Beamer'); + $this->assertNotNull($beamer); + + $beamer->setColorHex('#FF8800'); + $this->assertSame('#FF8800', $beamer->getColorHex()); + } + + #[Test] + public function removeLabelRemovesFromDocument(): void + { + $library = LabelsFileReader::read(self::REFERENCE_PATH); + $countBefore = $library->count(); + + $this->assertTrue($library->removeLabel('Leere Folie')); + $this->assertSame($countBefore - 1, $library->count()); + $this->assertNull($library->getLabelByName('Leere Folie')); + } +} diff --git a/tests/MacrosFileWriterTest.php b/tests/MacrosFileWriterTest.php new file mode 100644 index 0000000..e5aa091 --- /dev/null +++ b/tests/MacrosFileWriterTest.php @@ -0,0 +1,83 @@ +assertSame(strlen($original), strlen($written)); + } finally { + @unlink($tmp); + } + } + + #[Test] + public function readBackPreservesMacrosAndCollections(): void + { + $library = MacrosFileReader::read(self::REFERENCE_PATH); + + $tmp = tempnam(sys_get_temp_dir(), 'macros_'); + try { + MacrosFileWriter::write($library, $tmp); + $reload = MacrosFileReader::read($tmp); + + $this->assertCount(24, $reload->getMacros()); + $this->assertCount(3, $reload->getCollections()); + $this->assertNotNull($reload->getMacroByName('Gottesdienst START')); + } finally { + @unlink($tmp); + } + } + + #[Test] + public function addMacroPersistsThroughWriteRead(): void + { + $library = MacrosFileReader::read(self::REFERENCE_PATH); + $library->addMacro('TestMacro', '11111111-1111-1111-1111-111111111111'); + + $tmp = tempnam(sys_get_temp_dir(), 'macros_'); + try { + MacrosFileWriter::write($library, $tmp); + $reload = MacrosFileReader::read($tmp); + + $found = $reload->getMacroByUuid('11111111-1111-1111-1111-111111111111'); + $this->assertNotNull($found); + $this->assertSame('TestMacro', $found->getName()); + } finally { + @unlink($tmp); + } + } + + #[Test] + public function setColorHexUpdatesMacro(): void + { + $library = MacrosFileReader::read(self::REFERENCE_PATH); + $macro = $library->getMacroByName('Gottesdienst START'); + $this->assertNotNull($macro); + + $macro->setColor(['r' => 0.5, 'g' => 0.0, 'b' => 1.0]); + $color = $macro->getColor(); + $this->assertNotNull($color); + $this->assertEqualsWithDelta(0.5, $color['r'], 0.001); + $this->assertEqualsWithDelta(0.0, $color['g'], 0.001); + $this->assertEqualsWithDelta(1.0, $color['b'], 0.001); + } +} diff --git a/tests/MessagesFileReaderTest.php b/tests/MessagesFileReaderTest.php new file mode 100644 index 0000000..dbc325d --- /dev/null +++ b/tests/MessagesFileReaderTest.php @@ -0,0 +1,88 @@ +expectException(InvalidArgumentException::class); + MessagesFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-messages'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = MessagesFileReader::read(self::REFERENCE_PATH); + $this->assertInstanceOf(MessageLibrary::class, $library); + $this->assertCount(2, $library->getMessages()); + $this->assertSame(2, $library->count()); + } + + #[Test] + public function messagesExposeTitleAndUuid(): void + { + $first = MessagesFileReader::read(self::REFERENCE_PATH)->getMessages()[0]; + $this->assertInstanceOf(Message::class, $first); + $this->assertSame('Gottesdienst Sonntag 10Uhr Timer', $first->getTitle()); + $this->assertSame('5D1DAC57-CD17-4AD0-A096-CCCB37FF425B', $first->getUuid()); + } + + #[Test] + public function lookupByUuidIsCaseInsensitive(): void + { + $library = MessagesFileReader::read(self::REFERENCE_PATH); + $upper = $library->getMessageByUuid('5D1DAC57-CD17-4AD0-A096-CCCB37FF425B'); + $lower = $library->getMessageByUuid('5d1dac57-cd17-4ad0-a096-cccb37ff425b'); + $this->assertNotNull($upper); + $this->assertSame($upper, $lower); + } + + #[Test] + public function lookupByNameSucceeds(): void + { + $message = MessagesFileReader::read(self::REFERENCE_PATH)->getMessageByName('Neue Nachricht'); + $this->assertNotNull($message); + $this->assertSame('68F5B8E9-7EA8-4259-A990-D1863BC56C78', $message->getUuid()); + } + + #[Test] + public function addAndRemoveMessageRoundTrip(): void + { + $library = MessagesFileReader::read(self::REFERENCE_PATH); + $library->addMessage('Test Message', '11111111-1111-1111-1111-111111111111'); + $this->assertSame(3, $library->count()); + $this->assertNotNull($library->getMessageByUuid('11111111-1111-1111-1111-111111111111')); + $this->assertTrue($library->removeMessage('11111111-1111-1111-1111-111111111111')); + $this->assertSame(2, $library->count()); + } + + #[Test] + public function writerProducesByteIdenticalRoundTrip(): void + { + $library = MessagesFileReader::read(self::REFERENCE_PATH); + $first = tempnam(sys_get_temp_dir(), 'messages_'); + $second = tempnam(sys_get_temp_dir(), 'messages_'); + try { + MessagesFileWriter::write($library, $first); + MessagesFileWriter::write(MessagesFileReader::read($first), $second); + $this->assertSame(file_get_contents($first), file_get_contents($second)); + } finally { + @unlink($first ?: ''); + @unlink($second ?: ''); + } + } +} diff --git a/tests/PropsFileReaderTest.php b/tests/PropsFileReaderTest.php new file mode 100644 index 0000000..fc96306 --- /dev/null +++ b/tests/PropsFileReaderTest.php @@ -0,0 +1,83 @@ +expectException(InvalidArgumentException::class); + PropsFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-props'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = PropsFileReader::read(self::REFERENCE_PATH); + $this->assertInstanceOf(PropLibrary::class, $library); + $this->assertCount(13, $library->getProps()); + $this->assertSame(13, $library->count()); + } + + #[Test] + public function propExposesNameAndUuid(): void + { + $prop = PropsFileReader::read(self::REFERENCE_PATH)->getProps()[0]; + $this->assertInstanceOf(Prop::class, $prop); + $this->assertSame('Props #1', $prop->getName()); + $this->assertSame('1FB23674-4341-4257-A376-E7E7318E84EF', $prop->getUuid()); + $this->assertTrue($prop->isEnabled()); + } + + #[Test] + public function lookupByUuidIsCaseInsensitive(): void + { + $library = PropsFileReader::read(self::REFERENCE_PATH); + $upper = $library->getPropByUuid('1FB23674-4341-4257-A376-E7E7318E84EF'); + $lower = $library->getPropByUuid('1fb23674-4341-4257-a376-e7e7318e84ef'); + $this->assertNotNull($upper); + $this->assertSame($upper, $lower); + } + + #[Test] + public function writerProducesStableRoundTrip(): void + { + $tmp = tempnam(sys_get_temp_dir(), 'props_'); + $second = tempnam(sys_get_temp_dir(), 'props_'); + try { + PropsFileWriter::write(PropsFileReader::read(self::REFERENCE_PATH), $tmp); + PropsFileWriter::write(PropsFileReader::read($tmp), $second); + $this->assertSame(file_get_contents($tmp), file_get_contents($second)); + } finally { + @unlink($tmp); + @unlink($second); + } + } + + #[Test] + public function addAndRemovePropRoundTrip(): void + { + $library = PropsFileReader::read(self::REFERENCE_PATH); + $prop = new Prop(new Cue()); + $prop->setName('Test Prop')->setUuid('11111111-1111-1111-1111-111111111111')->setEnabled(true); + $library->addProp($prop); + $this->assertSame(14, $library->count()); + $this->assertSame('Test Prop', $library->getPropByUuid('11111111-1111-1111-1111-111111111111')?->getName()); + $this->assertTrue($library->removeProp('11111111-1111-1111-1111-111111111111')); + $this->assertSame(13, $library->count()); + } +} diff --git a/tests/StageFileReaderTest.php b/tests/StageFileReaderTest.php new file mode 100644 index 0000000..3ae1ac0 --- /dev/null +++ b/tests/StageFileReaderTest.php @@ -0,0 +1,86 @@ +expectException(InvalidArgumentException::class); + StageFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-stage'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = StageFileReader::read(self::REFERENCE_PATH); + $this->assertInstanceOf(StageLibrary::class, $library); + $this->assertCount(12, $library->getLayouts()); + $this->assertSame(12, $library->count()); + } + + #[Test] + public function layoutExposesNameAndUuid(): void + { + $layout = StageFileReader::read(self::REFERENCE_PATH)->getLayouts()[0]; + $this->assertInstanceOf(StageLayout::class, $layout); + $this->assertSame('Default StageDisplay', $layout->getName()); + $this->assertSame('0455674A-3F5C-4A62-B944-C276F3DF6F4E', $layout->getUuid()); + $this->assertNotNull($layout->getSlide()); + } + + #[Test] + public function lookupByUuidIsCaseInsensitive(): void + { + $library = StageFileReader::read(self::REFERENCE_PATH); + $upper = $library->getLayoutByUuid('0455674A-3F5C-4A62-B944-C276F3DF6F4E'); + $lower = $library->getLayoutByUuid('0455674a-3f5c-4a62-b944-c276f3df6f4e'); + $this->assertNotNull($upper); + $this->assertSame($upper, $lower); + } + + #[Test] + public function writerProducesStableRoundTrip(): void + { + $library = StageFileReader::read(self::REFERENCE_PATH); + $tmp = tempnam(sys_get_temp_dir(), 'stage_'); + $second = tempnam(sys_get_temp_dir(), 'stage_'); + try { + StageFileWriter::write($library, $tmp); + StageFileWriter::write(StageFileReader::read($tmp), $second); + $this->assertSame(file_get_contents($tmp), file_get_contents($second)); + } finally { + @unlink($tmp); + @unlink($second); + } + } + + #[Test] + public function addAndRemoveLayoutRoundTrip(): void + { + $library = StageFileReader::read(self::REFERENCE_PATH); + $layout = new StageLayout(new LayoutProto()); + $layout->setName('Test Layout')->setUuid('11111111-1111-1111-1111-111111111111'); + + $library->addLayout($layout); + $this->assertSame(13, $library->count()); + $this->assertSame('Test Layout', $library->getLayoutByUuid('11111111-1111-1111-1111-111111111111')?->getName()); + + $this->assertTrue($library->removeLayout('11111111-1111-1111-1111-111111111111')); + $this->assertSame(12, $library->count()); + } +} diff --git a/tests/TestPatternsFileReaderTest.php b/tests/TestPatternsFileReaderTest.php new file mode 100644 index 0000000..acba56c --- /dev/null +++ b/tests/TestPatternsFileReaderTest.php @@ -0,0 +1,99 @@ +expectException(InvalidArgumentException::class); + TestPatternsFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-test-patterns'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = TestPatternsFileReader::read(self::REFERENCE_PATH); + + $this->assertInstanceOf(TestPatternsLibrary::class, $library); + $this->assertCount(0, $library->getPatterns()); + $this->assertSame(0, $library->count()); + } + + #[Test] + public function stateExposesDisplayLocationAndSpecificScreenUuid(): void + { + $library = TestPatternsFileReader::read(self::REFERENCE_PATH); + + $this->assertNotNull($library->getState()); + $this->assertSame(3, $library->getDisplayLocation()); + $this->assertSame('BCDE1115-AD40-4BA4-A33A-BFFE3E87223B', $library->getSpecificScreenUuid()); + } + + #[Test] + public function lookupByUuidIsCaseInsensitive(): void + { + $library = TestPatternsFileReader::read(self::REFERENCE_PATH); + + $library->addPattern('Test Pattern', 'ABCDEFAB-1111-1111-1111-111111111111'); + $upper = $library->getPatternByUuid('ABCDEFAB-1111-1111-1111-111111111111'); + $lower = $library->getPatternByUuid('abcdefab-1111-1111-1111-111111111111'); + + $this->assertNotNull($upper); + $this->assertSame($upper, $lower); + $this->assertSame('Test Pattern', $upper->getNameLocalizationKey()); + } + + #[Test] + public function lookupByNameSucceeds(): void + { + $library = TestPatternsFileReader::read(self::REFERENCE_PATH); + + $library->addPattern('Test Pattern', '11111111-1111-1111-1111-111111111111'); + $pattern = $library->getPatternByName('Test Pattern'); + + $this->assertNotNull($pattern); + $this->assertSame('11111111-1111-1111-1111-111111111111', $pattern->getUuid()?->getString()); + } + + #[Test] + public function addAndRemovePatternRoundTrip(): void + { + $library = TestPatternsFileReader::read(self::REFERENCE_PATH); + + $library->addPattern('Test Pattern', '11111111-1111-1111-1111-111111111111'); + $this->assertNotNull($library->getPatternByUuid('11111111-1111-1111-1111-111111111111')); + $this->assertSame(1, $library->count()); + + $this->assertTrue($library->removePattern('11111111-1111-1111-1111-111111111111')); + $this->assertNull($library->getPatternByUuid('11111111-1111-1111-1111-111111111111')); + $this->assertSame(0, $library->count()); + } + + #[Test] + public function writerProducesByteIdenticalRoundTrip(): void + { + $original = file_get_contents(self::REFERENCE_PATH); + $library = TestPatternsFileReader::read(self::REFERENCE_PATH); + + $tmp = tempnam(sys_get_temp_dir(), 'test_patterns_'); + try { + TestPatternsFileWriter::write($library, $tmp); + $this->assertSame($original, file_get_contents($tmp)); + } finally { + @unlink($tmp); + } + } +} diff --git a/tests/ThemeFileReaderTest.php b/tests/ThemeFileReaderTest.php new file mode 100644 index 0000000..db488e2 --- /dev/null +++ b/tests/ThemeFileReaderTest.php @@ -0,0 +1,148 @@ +expectException(InvalidArgumentException::class); + ThemeFileReader::read(__DIR__ . '/../doc/reference_samples/pp-themes/does-not-exist'); + } + + #[Test] + public function readReturnsBundleWithExpectedCount(): void + { + $bundle = ThemeFileReader::read(self::REFERENCE_PATH); + $this->assertInstanceOf(ThemeBundle::class, $bundle); + $this->assertCount(11, $bundle->getSlides()); + $this->assertSame(11, $bundle->count()); + $this->assertCount(3, $bundle->getAssets()); + $this->assertSame(3, $bundle->getAssetCount()); + } + + #[Test] + public function slideExposesNameAndBaseSlide(): void + { + $slide = ThemeFileReader::read(self::REFERENCE_PATH)->getSlides()[0]; + $this->assertInstanceOf(ThemeSlide::class, $slide); + $this->assertSame('KeyVisual', $slide->getName()); + $this->assertNotNull($slide->getBaseSlide()); + } + + #[Test] + public function assetsExposeBytesAndMimeType(): void + { + $asset = ThemeFileReader::read(self::REFERENCE_PATH)->getAssetByName('BACKGROUND.jpg'); + $this->assertInstanceOf(ThemeAsset::class, $asset); + $this->assertSame('BACKGROUND.jpg', $asset->getName()); + $this->assertNotSame('', $asset->getBytes()); + $this->assertGreaterThan(0, $asset->getSize()); + $this->assertSame('image/jpeg', $asset->getMimeType()); + } + + #[Test] + public function writerProducesStableThemeDocumentRoundTrip(): void + { + $tmp = $this->makeTempDir('theme_'); + $second = $this->makeTempDir('theme_'); + try { + ThemeFileWriter::write(ThemeFileReader::read(self::REFERENCE_PATH), $tmp); + ThemeFileWriter::write(ThemeFileReader::read($tmp), $second); + $this->assertSame(file_get_contents($tmp . '/Theme'), file_get_contents($second . '/Theme')); + } finally { + $this->removeDirectory($tmp); + $this->removeDirectory($second); + } + } + + #[Test] + public function writerRoundTripsEntireFolder(): void + { + $source = ThemeFileReader::read(self::REFERENCE_PATH); + $tmp = $this->makeTempDir('theme_'); + try { + ThemeFileWriter::write($source, $tmp); + $roundTrip = ThemeFileReader::read($tmp); + $this->assertSame($source->count(), $roundTrip->count()); + $this->assertSame($source->getAssetCount(), $roundTrip->getAssetCount()); + $first = $source->getAssets()[0]; + $this->assertSame($first->getBytes(), $roundTrip->getAssetByName($first->getName())?->getBytes()); + } finally { + $this->removeDirectory($tmp); + } + } + + #[Test] + public function addRemoveAndStaleAssetCleanupRoundTrip(): void + { + $bundle = ThemeFileReader::read(self::REFERENCE_PATH); + $slide = new ThemeSlide(new TemplateSlide()); + $slide->setName('Test Theme Slide'); + $bundle->addSlide($slide); + $bundle->addAsset('TEST.png', 'png-bytes'); + $this->assertSame(12, $bundle->count()); + $this->assertSame(4, $bundle->getAssetCount()); + + $this->assertTrue($bundle->removeSlide('Test Theme Slide')); + $this->assertTrue($bundle->removeAsset('TEST.png')); + $this->assertSame(11, $bundle->count()); + $this->assertSame(3, $bundle->getAssetCount()); + + $tmp = $this->makeTempDir('theme_'); + try { + mkdir($tmp . '/Assets'); + file_put_contents($tmp . '/Assets/stale.jpg', 'stale'); + ThemeFileWriter::write($bundle, $tmp); + $this->assertFileDoesNotExist($tmp . '/Assets/stale.jpg'); + } finally { + $this->removeDirectory($tmp); + } + } + + private function makeTempDir(string $prefix): string + { + $path = sys_get_temp_dir() . '/' . $prefix . uniqid('', true); + mkdir($path); + + return $path; + } + + private function removeDirectory(string $path): void + { + if (!is_dir($path)) { + return; + } + $entries = scandir($path); + if ($entries === false) { + return; + } + foreach ($entries as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + $child = $path . '/' . $entry; + if (is_dir($child)) { + $this->removeDirectory($child); + } else { + @unlink($child); + } + } + @rmdir($path); + } +} diff --git a/tests/TimersFileReaderTest.php b/tests/TimersFileReaderTest.php new file mode 100644 index 0000000..fc0d7bd --- /dev/null +++ b/tests/TimersFileReaderTest.php @@ -0,0 +1,94 @@ +expectException(InvalidArgumentException::class); + TimersFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-timers'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = TimersFileReader::read(self::REFERENCE_PATH); + $this->assertInstanceOf(TimersLibrary::class, $library); + $this->assertCount(5, $library->getTimers()); + $this->assertSame(5, $library->count()); + } + + #[Test] + public function timersExposeNameAndUuid(): void + { + $first = TimersFileReader::read(self::REFERENCE_PATH)->getTimers()[0]; + $this->assertInstanceOf(Timer::class, $first); + $this->assertSame('Gottesdienst (10:02)', $first->getName()); + $this->assertSame('0E45D0AF-BCC2-4A31-BCFD-0F5A3358E225', $first->getUuid()); + } + + #[Test] + public function lookupByUuidIsCaseInsensitiveAndClockFormatIsExposed(): void + { + $library = TimersFileReader::read(self::REFERENCE_PATH); + $upper = $library->getTimerByUuid('0E45D0AF-BCC2-4A31-BCFD-0F5A3358E225'); + $lower = $library->getTimerByUuid('0e45d0af-bcc2-4a31-bcfd-0f5a3358e225'); + $this->assertNotNull($upper); + $this->assertSame($upper, $lower); + $this->assertSame('HH:mm', $library->getClockFormat()); + } + + #[Test] + public function timerTypesAreIdentified(): void + { + $library = TimersFileReader::read(self::REFERENCE_PATH); + $service = $library->getTimerByName('Gottesdienst (10:02)'); + $five = $library->getTimerByName('5 Minuten Countdown'); + $this->assertNotNull($service); + $this->assertTrue($service->isCountdownToTime()); + $this->assertFalse($service->isCountdown()); + $this->assertNotNull($five); + $this->assertTrue($five->isCountdown()); + $this->assertSame(300, $five->getDurationSeconds()); + } + + #[Test] + public function addAndRemoveTimerRoundTrip(): void + { + $library = TimersFileReader::read(self::REFERENCE_PATH); + $library->addTimer('Test Timer', '11111111-1111-1111-1111-111111111111'); + $this->assertSame(6, $library->count()); + $this->assertNotNull($library->getTimerByUuid('11111111-1111-1111-1111-111111111111')); + $this->assertTrue($library->removeTimer('11111111-1111-1111-1111-111111111111')); + $this->assertSame(5, $library->count()); + } + + #[Test] + public function writerProducesByteIdenticalRoundTrip(): void + { + $first = tempnam(sys_get_temp_dir(), 'timers_'); + $second = tempnam(sys_get_temp_dir(), 'timers_'); + try { + TimersFileWriter::write(TimersFileReader::read(self::REFERENCE_PATH), $first); + TimersFileWriter::write(TimersFileReader::read($first), $second); + $this->assertSame(file_get_contents($first), file_get_contents($second)); + } finally { + @unlink($first ?: ''); + @unlink($second ?: ''); + } + } +} diff --git a/tests/WorkspaceFileReaderTest.php b/tests/WorkspaceFileReaderTest.php new file mode 100644 index 0000000..770c152 --- /dev/null +++ b/tests/WorkspaceFileReaderTest.php @@ -0,0 +1,83 @@ +expectException(InvalidArgumentException::class); + WorkspaceFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-workspace'); + } + + #[Test] + public function readReturnsLibraryWithExpectedCount(): void + { + $library = WorkspaceFileReader::read(self::REFERENCE_PATH); + $this->assertInstanceOf(WorkspaceLibrary::class, $library); + $this->assertCount(5, $library->getScreens()); + $this->assertSame(5, $library->count()); + } + + #[Test] + public function screenExposesNameAndUuid(): void + { + $screen = WorkspaceFileReader::read(self::REFERENCE_PATH)->getScreens()[0]; + $this->assertInstanceOf(Screen::class, $screen); + $this->assertSame('StageDisplay', $screen->getName()); + $this->assertSame('C86D614D-9441-4F78-A177-03E6E5FFEDF8', $screen->getUuid()); + $this->assertSame(2, $screen->getScreenType()); + } + + #[Test] + public function lookupByUuidIsCaseInsensitive(): void + { + $library = WorkspaceFileReader::read(self::REFERENCE_PATH); + $upper = $library->getScreenByUuid('C86D614D-9441-4F78-A177-03E6E5FFEDF8'); + $lower = $library->getScreenByUuid('c86d614d-9441-4f78-a177-03e6e5ffedf8'); + $this->assertNotNull($upper); + $this->assertSame($upper, $lower); + } + + #[Test] + public function writerProducesStableRoundTrip(): void + { + $tmp = tempnam(sys_get_temp_dir(), 'workspace_'); + $second = tempnam(sys_get_temp_dir(), 'workspace_'); + try { + WorkspaceFileWriter::write(WorkspaceFileReader::read(self::REFERENCE_PATH), $tmp); + WorkspaceFileWriter::write(WorkspaceFileReader::read($tmp), $second); + $this->assertSame(file_get_contents($tmp), file_get_contents($second)); + } finally { + @unlink($tmp); + @unlink($second); + } + } + + #[Test] + public function addAndRemoveScreenRoundTrip(): void + { + $library = WorkspaceFileReader::read(self::REFERENCE_PATH); + $screen = new Screen(new ProPresenterScreen()); + $screen->setName('Test Screen')->setUuid('11111111-1111-1111-1111-111111111111'); + $library->addScreen($screen); + $this->assertSame(6, $library->count()); + $this->assertSame('Test Screen', $library->getScreenByUuid('11111111-1111-1111-1111-111111111111')?->getName()); + $this->assertTrue($library->removeScreen('11111111-1111-1111-1111-111111111111')); + $this->assertSame(5, $library->count()); + } +}