pp-planer/app/Services/MacroResolutionService.php
Thorsten Bus c3c8f7f50d fix(export): restructure keyvisual/nametag sequence and macro placement
- MacroResolutionService: rank by position specificity (by_label >
  first/last > all_slides) so a broad all_slides macro no longer masks
  first_slide/last_slide assignments
- ProBundleExportService: resolve macros in a second pass over emitted
  slides, so last_slide still matches when files were skipped
- PlaylistExportService: keyvisual is now a transition between content
  items instead of a one-shot fallback; nametags use a unified 3-cue
  sequence (kv -> kv+name 10s auto-next -> kv) for moderator and preacher
- PlaylistExportService: prepend countdown standby slide (clock + kv)
  when the add_countdown_slide setting is enabled
2026-08-03 08:59:54 +02:00

110 lines
4.1 KiB
PHP

<?php
namespace App\Services;
use App\Models\Macro;
use App\Models\MacroAssignment;
use App\Models\Service;
use App\Models\ServiceMacroAssignment;
use App\Models\ServiceMacroOverride;
use Illuminate\Support\Collection;
class MacroResolutionService
{
/**
* Returns active (non-hidden) assignments for a given service + part type.
* Uses service-specific assignments if an override exists, otherwise global defaults.
*/
public function resolveAssignmentsForPart(Service $service, string $partType): Collection
{
$hasOverride = ServiceMacroOverride::where('service_id', $service->id)
->where('part_type', $partType)
->exists();
if ($hasOverride) {
$rows = ServiceMacroAssignment::with(['macro', 'label'])
->where('service_id', $service->id)
->where('part_type', $partType)
->orderBy('order')
->get();
} else {
$rows = MacroAssignment::with(['macro', 'label'])
->where('part_type', $partType)
->orderBy('order')
->get();
}
return $rows
->reject(fn ($r) => $r->macro === null || $r->macro->isHidden())
->reject(fn ($r) => $r->position === 'by_label' && ($r->label === null || $r->label->isHidden()));
}
/**
* Returns the macro export data for macros that apply to a specific slide.
*
* @param array $slideContext ['index' => int, 'total' => int, 'label_id' => int|null]
* @return array<int, array{name: string, uuid: string, collectionName: string, collectionUuid: string}>
*/
public function macrosForSlide(Service $service, string $partType, array $slideContext): array
{
$assignments = $this->resolveAssignmentsForPart($service, $partType);
$matched = $assignments->filter(function ($a) use ($slideContext) {
return match ($a->position) {
'all_slides' => true,
'first_slide' => $slideContext['index'] === 0,
'last_slide' => $slideContext['index'] === $slideContext['total'] - 1,
'by_label' => isset($slideContext['label_id'])
&& (int) $a->label_id === (int) $slideContext['label_id'],
default => false,
};
});
// The ProPresenter parser stores only ONE macro per slide (consumers keep
// $macros[0]). Order the matches so the MOST SPECIFIC position wins,
// independent of the configured `order`: by_label > first_slide/last_slide
// > all_slides. Ties break by `order`. This stops a broad all_slides
// assignment from masking a first_slide/last_slide macro on the very
// first/last slide of a part.
$sorted = $matched
->sortBy(fn ($a) => $this->positionSpecificity($a->position) * 100000 + (int) $a->order)
->values();
return $sorted->map(fn ($a) => $this->toExportArray($a->macro))->values()->all();
}
/**
* Precedence rank for a macro position when several assignments match a single
* slide and only one macro can be emitted. Lower rank = more specific = wins.
*/
private function positionSpecificity(string $position): int
{
return match ($position) {
'by_label' => 0,
'first_slide', 'last_slide' => 1,
'all_slides' => 2,
default => 3,
};
}
/**
* Returns the count of active assignments for a service + part (for UI badges).
*/
public function countAssignmentsForPart(Service $service, string $partType): int
{
return $this->resolveAssignmentsForPart($service, $partType)->count();
}
private function toExportArray(Macro $macro): array
{
$collection = $macro->collections()->first();
return [
'name' => $macro->name,
'uuid' => $macro->uuid,
'collectionName' => $collection?->name ?? '--MAIN--',
'collectionUuid' => $collection?->uuid ?? '8D02FC57-83F8-4042-9B90-81C229728426',
];
}
}