- 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
110 lines
4.1 KiB
PHP
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',
|
|
];
|
|
}
|
|
}
|