From 196657b52bcf6d09e022a74c219167ff409b5448 Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Sun, 31 May 2026 00:37:23 +0200 Subject: [PATCH] feat(export): background layer on song/sermon slides --- .sisyphus/evidence/task-8-no-background.txt | 15 ++ .sisyphus/evidence/task-8-song-background.txt | 20 ++ .../keyvisual-background-nametag/learnings.md | 29 +++ app/Http/Controllers/SlideController.php | 2 + app/Models/Slide.php | 2 + app/Services/PlaylistExportService.php | 70 +++++- app/Services/ProBundleExportService.php | 78 ++++++- app/Services/ProExportService.php | 36 +++ ..._115950_add_cover_mode_to_slides_table.php | 22 ++ tests/Feature/ProFileExportTest.php | 211 ++++++++++++++++++ 10 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 .sisyphus/evidence/task-8-no-background.txt create mode 100644 .sisyphus/evidence/task-8-song-background.txt create mode 100644 database/migrations/2026_05_10_115950_add_cover_mode_to_slides_table.php diff --git a/.sisyphus/evidence/task-8-no-background.txt b/.sisyphus/evidence/task-8-no-background.txt new file mode 100644 index 0000000..e56c749 --- /dev/null +++ b/.sisyphus/evidence/task-8-no-background.txt @@ -0,0 +1,15 @@ +Task T8 evidence — no background / excluded slide types + +Verified by `Tests\Feature\ProFileExportTest`: +- `test_export_ohne_background_enthaelt_keine_background_actions` + - Service without resolved background generates successfully + - exported song slides contain zero BACKGROUND media actions +- `test_information_und_moderation_exports_erhalten_keinen_background` + - information bundle gets no BACKGROUND media + - moderation bundle gets no BACKGROUND media +- `test_playlist_export_setzt_background_auf_sermon_folien_und_nicht_auf_informationen` + - final playlist keeps information slides without BACKGROUND media + +Verification commands: +- `ddev exec php artisan test tests/Feature/ProFileExportTest.php` → 13 passed, 76 assertions +- `ddev exec php artisan test` → 537 passed, 2683 assertions diff --git a/.sisyphus/evidence/task-8-song-background.txt b/.sisyphus/evidence/task-8-song-background.txt new file mode 100644 index 0000000..9e8fbc9 --- /dev/null +++ b/.sisyphus/evidence/task-8-song-background.txt @@ -0,0 +1,20 @@ +Task T8 evidence — song/sermon background layer + +RED: +- `ddev exec php artisan test tests/Feature/ProFileExportTest.php` +- Failed as expected before implementation: + - song slides had no BACKGROUND media action + - slides table had no `cover_mode` column for full-cover detection + +GREEN: +- `ddev exec php artisan test tests/Feature/ProFileExportTest.php` +- 13 passed, 76 assertions +- Verified: + - every song text slide gets BACKGROUND media when ServiceImageResolver resolves a background + - sermon image slides get BACKGROUND media when not full-cover + - full-cover sermon image slides (`cover_mode=true`) skip BACKGROUND media + - final .proplaylist export preserves the same sermon/full-cover behavior + +Full suite: +- `ddev exec ./vendor/bin/pint ... && ddev exec php artisan test` +- 537 passed, 2683 assertions diff --git a/.sisyphus/notepads/keyvisual-background-nametag/learnings.md b/.sisyphus/notepads/keyvisual-background-nametag/learnings.md index 32aa98c..4433be3 100644 --- a/.sisyphus/notepads/keyvisual-background-nametag/learnings.md +++ b/.sisyphus/notepads/keyvisual-background-nametag/learnings.md @@ -59,3 +59,32 @@ ## [2026-05-31] Task: T5 - Return contract is the raw storage-relative filename (`slides/abc.jpg`), NOT the Service model `/storage/...` URL accessor output. - Test file `tests/Feature/ServiceImageResolverTest.php` covers service wins, global fallback, null, missing service-file fallthrough, and background resolution branches. - Full suite green: `ddev exec php artisan test` → 519 passed, 2627 assertions. Pint clean for resolver + tests. + +## [2026-05-31] Task: T6 — ServiceImageController +- New controller `app/Http/Controllers/ServiceImageController.php`: `storeKeyVisual()` + `storeBackground()` both delegate to private `store($request, $service, $column, $settingKey)`. +- Validation: `file` => required|file|mimes:jpg,jpeg,png|max:20480 (20MB); `scope` => required|Rule::in(['service','default']). German messages via 2nd arg to `$request->validate([...], [...])`. +- Conversion: `app(FileConversionService::class)->convertImageCover($request->file('file'))` → stores `$result['filename']` (slides/{uuid}.jpg) into `key_visual_filename`/`background_filename` via `$service->update([...])`. +- scope=default ALSO calls `Setting::set('current_key_visual'|'current_background', $result['filename'])`. scope=service leaves global Setting untouched. +- Old file NOT deleted on replace (protects finalized snapshots). Verified by test. +- Routes (inside auth group): `POST /services/{service}/key-visual` → `services.key-visual.store`; `POST /services/{service}/background` → `services.background.store`. +- Response: `back()->with('success', 'Bild wurde gespeichert.')` (Inertia redirect). Web validation failures redirect 302; tests use `postJson()` to assert the 422 JSON contract. +- GOTCHA: after adding controller, route cache + autoload were stale → `ddev exec composer dump-autoload && ddev exec php artisan optimize:clear`. Also the `use` import edit silently didn't stick first time — verify imports after editing routes/web.php. +- Test file `tests/Feature/ServiceImageControllerTest.php` (6 tests). Helper `makeImageUpload($name,$w,$h)` GD-based (same pattern as SlideControllerTest's makePngUploadForSlide). +- Full suite: 525 passed (519 baseline + 6). Pint clean. +- Evidence: `.sisyphus/evidence/task-6-default-upload.txt`, `task-6-invalid-upload.txt`. + +## [2026-05-31] Task: T7 — NameTagResolver +- New service `App\Services\NameTagResolver`: `moderatorFor(Service): ?string`, `preacherFor(Service): ?string`. +- Moderator resolution: trimmed `services.moderator_name` wins; else first visible agenda item (`is_before_event=false`) ordered by `sort_order`, then `id`; responsible names joined by `', '`; no name => `null`. +- Preacher resolution: trimmed `services.preacher_name_override` wins; else trimmed `services.preacher_name`; else first visible non-song sermon agenda item (`service_song_id IS NULL`) responsible names; no name => `null`. +- `responsible` JSON findings: model casts to array; sync test proves associative object shape `{name: 'Max Mustermann'}`; expected multi-person shape is list of objects `[{name: 'Anna Müller'}, {name: 'Tom Klein'}]`. Resolver supports both plus string entries and `firstName`/`lastName` or snake_case fallbacks. +- Sermon detection method: use `Setting::get('agenda_sermon_matching')` comma patterns through `AgendaMatcherService::matchesAny()` (same matcher used by ServiceController/Service finalization). If no setting is configured, fallback checks title/type for `predigt` or `sermon`. +- Tests: `tests/Feature/NameTagResolverTest.php` covers override/fallback/null branches for moderator and preacher. Full suite green: `ddev exec php artisan test` → 532 passed (2659 assertions). Pint clean. Evidence: `.sisyphus/evidence/task-7-moderator.txt`, `.sisyphus/evidence/task-7-preacher.txt`. + +## [2026-05-31] Task: T8 — Export background layer +- `ProExportService::buildGroups()` now resolves `ServiceImageResolver::backgroundFor($service)` once per song export and adds `slideData['background']` with `Storage::disk('public')->path(...)`, format JPG, 1920×1080 to every non-full-cover song slide. +- Sermon image exports use the same background contract in `PlaylistExportService` and `ProBundleExportService`; information/moderation slide exports explicitly remain without background. +- Full-cover detection is persisted with new nullable `slides.cover_mode` (`null` legacy/unknown, `false` contain, `true` cover). `SlideController` stores `$result['fullCover']` from image/ZIP conversions; existing contain conversions store `false`. +- Migration timestamp chosen as `2026_05_10_115950_add_cover_mode_to_slides_table.php` so the existing CCLI rollback test still rolls back the CCLI migration with `--step=1`. +- Tests appended to `tests/Feature/ProFileExportTest.php`: song background, null background, sermon full-cover skip, information/moderation exclusion, playlist sermon/information regression. +- Verification: RED targeted test failed before implementation; GREEN targeted `ProFileExportTest` 13 passed / 76 assertions; full suite `ddev exec php artisan test` 537 passed / 2683 assertions; Pint clean. Evidence: `.sisyphus/evidence/task-8-song-background.txt`, `.sisyphus/evidence/task-8-no-background.txt`. diff --git a/app/Http/Controllers/SlideController.php b/app/Http/Controllers/SlideController.php index d8033e1..587e454 100644 --- a/app/Http/Controllers/SlideController.php +++ b/app/Http/Controllers/SlideController.php @@ -169,6 +169,7 @@ private function handleImage( 'original_filename' => $file->getClientOriginalName(), 'stored_filename' => $result['filename'], 'thumbnail_filename' => $result['thumbnail'], + 'cover_mode' => $result['fullCover'] ?? null, 'expire_date' => $expireDate, 'uploader_name' => $uploaderName, 'uploaded_at' => now(), @@ -260,6 +261,7 @@ private function handleZip( 'original_filename' => $file->getClientOriginalName(), 'stored_filename' => $result['filename'], 'thumbnail_filename' => $result['thumbnail'], + 'cover_mode' => $result['fullCover'] ?? null, 'expire_date' => $expireDate, 'uploader_name' => $uploaderName, 'uploaded_at' => now(), diff --git a/app/Models/Slide.php b/app/Models/Slide.php index 90c0845..f94eb12 100644 --- a/app/Models/Slide.php +++ b/app/Models/Slide.php @@ -19,6 +19,7 @@ class Slide extends Model 'original_filename', 'stored_filename', 'thumbnail_filename', + 'cover_mode', 'expire_date', 'uploader_name', 'uploaded_at', @@ -30,6 +31,7 @@ protected function casts(): array return [ 'expire_date' => 'date', 'uploaded_at' => 'datetime', + 'cover_mode' => 'boolean', ]; } diff --git a/app/Services/PlaylistExportService.php b/app/Services/PlaylistExportService.php index 5c89a4b..2133a8b 100644 --- a/app/Services/PlaylistExportService.php +++ b/app/Services/PlaylistExportService.php @@ -73,6 +73,8 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda $tempDir, $playlistItems, $embeddedFiles, + $service, + 'information', ); $announcementInserted = true; } @@ -118,6 +120,8 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda $tempDir, $playlistItems, $embeddedFiles, + $service, + $this->backgroundPartTypeForAgendaItem($item), ); } } @@ -132,6 +136,8 @@ private function generatePlaylistFromAgenda(Service $service, Collection $agenda $tempDir, $prependItems, $prependFiles, + $service, + 'information', ); $playlistItems = array_merge($prependItems, $playlistItems); $embeddedFiles = array_merge($prependFiles, $embeddedFiles); @@ -260,9 +266,12 @@ private function addSlidesFromCollection( string $tempDir, array &$playlistItems, array &$embeddedFiles, + ?Service $service = null, + ?string $backgroundPartType = null, ): void { $slideDataList = []; $imageFiles = []; + $background = $this->backgroundData($service); foreach ($slides->values() as $index => $slide) { $storedPath = Storage::disk('public')->path($slide->stored_filename); @@ -277,11 +286,17 @@ private function addSlidesFromCollection( $imageFiles[$imageFilename] = file_get_contents($destPath); - $slideDataList[] = [ + $singleSlideData = [ 'media' => $imageFilename, 'format' => 'JPG', 'label' => $slide->original_filename, ]; + + if ($this->shouldAttachBackground($slide, $backgroundPartType, $background)) { + $singleSlideData['background'] = $background; + } + + $slideDataList[] = $singleSlideData; } if (empty($slideDataList)) { @@ -362,6 +377,8 @@ private function addSlidePresentation( $tempDir, $playlistItems, $embeddedFiles, + $service, + $type, ); } @@ -378,6 +395,57 @@ private function countSongLabels(\App\Models\Song $song): int ->sum('arrangement_labels_count'); } + private function backgroundData(?Service $service): ?array + { + if ($service === null) { + return null; + } + + $background = app(ServiceImageResolver::class)->backgroundFor($service); + + if ($background === null) { + return null; + } + + return [ + 'path' => Storage::disk('public')->path($background), + 'format' => 'JPG', + 'width' => 1920, + 'height' => 1080, + ]; + } + + private function shouldAttachBackground(Slide $slide, ?string $backgroundPartType, ?array $background): bool + { + if ($background === null) { + return false; + } + + if ($backgroundPartType !== 'sermon' && $slide->type !== 'sermon') { + return false; + } + + return $slide->cover_mode !== true; + } + + private function backgroundPartTypeForAgendaItem(ServiceAgendaItem $item): string + { + if ($item->slides->contains(fn (Slide $slide) => $slide->type === 'sermon')) { + return 'sermon'; + } + + $sermonPatterns = Setting::get('agenda_sermon_matching'); + if ($sermonPatterns === null) { + return 'agenda_item'; + } + + $patterns = array_map('trim', explode(',', $sermonPatterns)); + + return app(AgendaMatcherService::class)->matchesAny($item->title, $patterns) + ? 'sermon' + : 'agenda_item'; + } + protected function writePlaylistFile(string $path, string $name, array $items, array $embeddedFiles): void { ProPlaylistGenerator::generateAndWrite($path, $name, $items, $embeddedFiles); diff --git a/app/Services/ProBundleExportService.php b/app/Services/ProBundleExportService.php index 6e36ce3..4bc7ac8 100644 --- a/app/Services/ProBundleExportService.php +++ b/app/Services/ProBundleExportService.php @@ -4,6 +4,8 @@ use App\Models\Service; use App\Models\ServiceAgendaItem; +use App\Models\Setting; +use App\Models\Slide; use Illuminate\Support\Facades\Storage; use InvalidArgumentException; use ProPresenter\Parser\PresentationBundle; @@ -17,6 +19,7 @@ class ProBundleExportService public function __construct( private readonly MacroResolutionService $macroResolutionService, + private readonly ServiceImageResolver $imageResolver, ) {} public function generateBundle(Service $service, string $blockType): string @@ -32,7 +35,7 @@ public function generateBundle(Service $service, string $blockType): string $groupName = ucfirst($blockType); - return $this->buildBundleFromSlides($slides, $groupName, $service, $blockType); + return $this->buildBundleFromSlides($slides, $groupName, $service, $blockType, $blockType); } public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string @@ -72,14 +75,26 @@ public function generateAgendaItemBundle(ServiceAgendaItem $agendaItem): string ->orderBy('sort_order') ->get(); - return $this->buildBundleFromSlides($slides, $title, $agendaItem->service, 'agenda_item'); + return $this->buildBundleFromSlides( + $slides, + $title, + $agendaItem->service, + 'agenda_item', + $this->backgroundPartTypeForAgendaItem($agendaItem), + ); } /** @param \Illuminate\Database\Eloquent\Collection $slides */ - private function buildBundleFromSlides($slides, string $groupName, ?Service $service = null, ?string $partType = null): string - { + private function buildBundleFromSlides( + $slides, + string $groupName, + ?Service $service = null, + ?string $partType = null, + ?string $backgroundPartType = null, + ): string { $slideData = []; $mediaFiles = []; + $background = $this->backgroundData($service); foreach ($slides as $slide) { $sourcePath = Storage::disk('public')->path($slide->stored_filename); @@ -101,6 +116,10 @@ private function buildBundleFromSlides($slides, string $groupName, ?Service $ser 'label' => $slide->original_filename, ]; + if ($this->shouldAttachBackground($slide, $backgroundPartType, $background)) { + $singleSlideData['background'] = $background; + } + if ($service !== null && $partType !== null) { $slideIndex = count($slideData); $totalSlides = $slides->count(); @@ -144,6 +163,57 @@ private function buildBundleFromSlides($slides, string $groupName, ?Service $ser return $bundlePath; } + private function backgroundData(?Service $service): ?array + { + if ($service === null) { + return null; + } + + $background = $this->imageResolver->backgroundFor($service); + + if ($background === null) { + return null; + } + + return [ + 'path' => Storage::disk('public')->path($background), + 'format' => 'JPG', + 'width' => 1920, + 'height' => 1080, + ]; + } + + private function shouldAttachBackground(Slide $slide, ?string $backgroundPartType, ?array $background): bool + { + if ($background === null) { + return false; + } + + if ($backgroundPartType !== 'sermon' && $slide->type !== 'sermon') { + return false; + } + + return $slide->cover_mode !== true; + } + + private function backgroundPartTypeForAgendaItem(ServiceAgendaItem $agendaItem): string + { + if ($agendaItem->slides->contains(fn (Slide $slide) => $slide->type === 'sermon')) { + return 'sermon'; + } + + $sermonPatterns = Setting::get('agenda_sermon_matching'); + if ($sermonPatterns === null) { + return 'agenda_item'; + } + + $patterns = array_map('trim', explode(',', $sermonPatterns)); + + return app(AgendaMatcherService::class)->matchesAny($agendaItem->title, $patterns) + ? 'sermon' + : 'agenda_item'; + } + private static function safeFilename(string $name): string { return preg_replace('/[^a-zA-Z0-9äöüÄÖÜß\-_ ]/', '', $name) ?: 'presentation'; diff --git a/app/Services/ProExportService.php b/app/Services/ProExportService.php index b73ba1d..825d135 100644 --- a/app/Services/ProExportService.php +++ b/app/Services/ProExportService.php @@ -4,12 +4,14 @@ use App\Models\Service; use App\Models\Song; +use Illuminate\Support\Facades\Storage; use ProPresenter\Parser\ProFileGenerator; class ProExportService { public function __construct( private readonly MacroResolutionService $macroResolutionService, + private readonly ServiceImageResolver $imageResolver, ) {} public function generateProFile(Song $song, ?Service $service = null): string @@ -51,6 +53,7 @@ private function buildGroups(Song $song, ?Service $service = null): array $groups = []; $seenLabelIds = []; + $background = $this->backgroundData($service); foreach ($defaultArr->arrangementLabels->sortBy('order') as $arrangementLabel) { $label = $arrangementLabel->label; @@ -75,6 +78,10 @@ private function buildGroups(Song $song, ?Service $service = null): array $slideData['translation'] = $slide->text_content_translated; } + if ($background !== null && ! $this->isFullCoverImageSlide($slide, $slideData)) { + $slideData['background'] = $background; + } + if ($service !== null) { $macros = $this->macroResolutionService->macrosForSlide( $service, @@ -101,6 +108,35 @@ private function buildGroups(Song $song, ?Service $service = null): array return $groups; } + private function backgroundData(?Service $service): ?array + { + if ($service === null) { + return null; + } + + $background = $this->imageResolver->backgroundFor($service); + + if ($background === null) { + return null; + } + + return [ + 'path' => Storage::disk('public')->path($background), + 'format' => 'JPG', + 'width' => 1920, + 'height' => 1080, + ]; + } + + private function isFullCoverImageSlide(object $slide, array $slideData): bool + { + if (! isset($slideData['media'])) { + return false; + } + + return ($slide->cover_mode ?? null) === true; + } + private function buildArrangements(Song $song): array { $arrangements = []; diff --git a/database/migrations/2026_05_10_115950_add_cover_mode_to_slides_table.php b/database/migrations/2026_05_10_115950_add_cover_mode_to_slides_table.php new file mode 100644 index 0000000..7450b48 --- /dev/null +++ b/database/migrations/2026_05_10_115950_add_cover_mode_to_slides_table.php @@ -0,0 +1,22 @@ +boolean('cover_mode')->nullable()->after('thumbnail_filename'); + }); + } + + public function down(): void + { + Schema::table('slides', function (Blueprint $table) { + $table->dropColumn('cover_mode'); + }); + } +}; diff --git a/tests/Feature/ProFileExportTest.php b/tests/Feature/ProFileExportTest.php index 33b230a..52550ed 100644 --- a/tests/Feature/ProFileExportTest.php +++ b/tests/Feature/ProFileExportTest.php @@ -7,10 +7,17 @@ use App\Models\MacroAssignment; use App\Models\MacroCollection; use App\Models\Service; +use App\Models\ServiceAgendaItem; +use App\Models\Slide; use App\Models\Song; use App\Models\User; +use App\Services\PlaylistExportService; +use App\Services\ProBundleExportService; use App\Services\ProExportService; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Storage; +use ProPresenter\Parser\ProBundleReader; +use ProPresenter\Parser\ProPlaylistReader; use Tests\TestCase; final class ProFileExportTest extends TestCase @@ -268,6 +275,187 @@ public function test_export_mit_ausgeblendeter_macro_enthaelt_keine_macro(): voi } } + public function test_export_mit_service_background_enthaelt_background_auf_allen_song_folien(): void + { + Storage::fake('public'); + Storage::disk('public')->put('slides/background.jpg', 'background-image'); + + $service = Service::factory()->create([ + 'background_filename' => 'slides/background.jpg', + ]); + $song = $this->createSongWithContent(); + $expectedPath = Storage::disk('public')->path('slides/background.jpg'); + + $parserSong = app(ProExportService::class)->generateParserSong($song, $service); + $slides = $this->allParserSlides($parserSong); + + $this->assertNotEmpty($slides); + foreach ($slides as $slide) { + $this->assertTrue($slide->hasBackgroundMedia()); + $this->assertSame($expectedPath, $slide->getBackgroundMediaUrl()); + $this->assertSame('JPG', $slide->getBackgroundMediaFormat()); + } + } + + public function test_export_ohne_background_enthaelt_keine_background_actions(): void + { + Storage::fake('public'); + + $service = Service::factory()->create(); + $song = $this->createSongWithContent(); + + $parserSong = app(ProExportService::class)->generateParserSong($song, $service); + + foreach ($this->allParserSlides($parserSong) as $slide) { + $this->assertFalse($slide->hasBackgroundMedia()); + } + } + + public function test_sermon_export_ueberspringt_background_bei_full_cover_folien(): void + { + Storage::fake('public'); + Storage::disk('public')->put('slides/background.jpg', 'background-image'); + Storage::disk('public')->put('slides/contain.jpg', 'contain-image'); + Storage::disk('public')->put('slides/cover.jpg', 'cover-image'); + + $service = Service::factory()->create([ + 'background_filename' => 'slides/background.jpg', + ]); + + Slide::factory()->create([ + 'service_id' => $service->id, + 'type' => 'sermon', + 'original_filename' => 'contain.jpg', + 'stored_filename' => 'slides/contain.jpg', + 'cover_mode' => false, + 'sort_order' => 0, + ]); + Slide::factory()->create([ + 'service_id' => $service->id, + 'type' => 'sermon', + 'original_filename' => 'cover.jpg', + 'stored_filename' => 'slides/cover.jpg', + 'cover_mode' => true, + 'sort_order' => 1, + ]); + + $bundlePath = app(ProBundleExportService::class)->generateBundle($service, 'sermon'); + $slides = $this->allParserSlides(ProBundleReader::read($bundlePath)->getSong()); + + $this->assertCount(2, $slides); + $this->assertTrue($slides[0]->hasBackgroundMedia()); + $this->assertFalse($slides[1]->hasBackgroundMedia()); + + @unlink($bundlePath); + } + + public function test_information_und_moderation_exports_erhalten_keinen_background(): void + { + Storage::fake('public'); + Storage::disk('public')->put('slides/background.jpg', 'background-image'); + Storage::disk('public')->put('slides/information.jpg', 'information-image'); + Storage::disk('public')->put('slides/moderation.jpg', 'moderation-image'); + + $service = Service::factory()->create([ + 'background_filename' => 'slides/background.jpg', + ]); + + Slide::factory()->create([ + 'service_id' => $service->id, + 'type' => 'information', + 'original_filename' => 'information.jpg', + 'stored_filename' => 'slides/information.jpg', + 'sort_order' => 0, + ]); + Slide::factory()->create([ + 'service_id' => $service->id, + 'type' => 'moderation', + 'original_filename' => 'moderation.jpg', + 'stored_filename' => 'slides/moderation.jpg', + 'sort_order' => 0, + ]); + + foreach (['information', 'moderation'] as $blockType) { + $bundlePath = app(ProBundleExportService::class)->generateBundle($service, $blockType); + + foreach ($this->allParserSlides(ProBundleReader::read($bundlePath)->getSong()) as $slide) { + $this->assertFalse($slide->hasBackgroundMedia()); + } + + @unlink($bundlePath); + } + } + + public function test_playlist_export_setzt_background_auf_sermon_folien_und_nicht_auf_informationen(): void + { + Storage::fake('public'); + Storage::disk('public')->put('slides/background.jpg', 'background-image'); + Storage::disk('public')->put('slides/information.jpg', 'information-image'); + Storage::disk('public')->put('slides/sermon-contain.jpg', 'sermon-contain-image'); + Storage::disk('public')->put('slides/sermon-cover.jpg', 'sermon-cover-image'); + + $service = Service::factory()->create([ + 'title' => 'Playlist Background', + 'date' => now(), + 'background_filename' => 'slides/background.jpg', + ]); + + Slide::factory()->create([ + 'service_id' => null, + 'type' => 'information', + 'original_filename' => 'information.jpg', + 'stored_filename' => 'slides/information.jpg', + 'uploaded_at' => now()->subDay(), + 'sort_order' => 0, + ]); + + $sermonItem = ServiceAgendaItem::factory()->create([ + 'service_id' => $service->id, + 'title' => 'Predigt', + 'service_song_id' => null, + 'sort_order' => 1, + 'is_before_event' => false, + ]); + + Slide::factory()->create([ + 'service_id' => $service->id, + 'service_agenda_item_id' => $sermonItem->id, + 'type' => 'sermon', + 'original_filename' => 'sermon-contain.jpg', + 'stored_filename' => 'slides/sermon-contain.jpg', + 'cover_mode' => false, + 'sort_order' => 0, + ]); + Slide::factory()->create([ + 'service_id' => $service->id, + 'service_agenda_item_id' => $sermonItem->id, + 'type' => 'sermon', + 'original_filename' => 'sermon-cover.jpg', + 'stored_filename' => 'slides/sermon-cover.jpg', + 'cover_mode' => true, + 'sort_order' => 1, + ]); + + $result = app(PlaylistExportService::class)->generatePlaylist($service); + $playlist = ProPlaylistReader::read($result['path']); + $informationSong = $playlist->getEmbeddedSong('Informationen.pro'); + $sermonSong = $playlist->getEmbeddedSong('Predigt.pro'); + + $this->assertNotNull($informationSong); + $this->assertNotNull($sermonSong); + + foreach ($this->allParserSlides($informationSong) as $slide) { + $this->assertFalse($slide->hasBackgroundMedia()); + } + + $sermonSlides = $this->allParserSlides($sermonSong); + $this->assertCount(2, $sermonSlides); + $this->assertTrue($sermonSlides[0]->hasBackgroundMedia()); + $this->assertFalse($sermonSlides[1]->hasBackgroundMedia()); + + $this->cleanupTempDir($result['temp_dir']); + } + private function createMacroForExport(string $name, array $attributes = []): Macro { $macro = Macro::factory()->create(array_merge([ @@ -305,4 +493,27 @@ private function assertStringContains(string $needle, ?string $haystack): void "Failed asserting that '{$haystack}' contains '{$needle}'" ); } + + private function cleanupTempDir(string $dir): void + { + if (! is_dir($dir)) { + return; + } + + $items = scandir($dir); + if ($items === false) { + return; + } + + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $dir.'/'.$item; + is_dir($path) ? $this->cleanupTempDir($path) : unlink($path); + } + + rmdir($dir); + } }