Display CMS Optimierungen 03-06-2026

This commit is contained in:
Kevin Adametz 2026-06-03 15:43:18 +02:00
parent 6c6d683b9a
commit bebfa63e87
17 changed files with 925 additions and 133 deletions

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use RuntimeException;
class PublishFluxAssets extends Command
{
protected $signature = 'flux:publish-assets {--force : Overwrite existing published Flux assets}';
protected $description = 'Publish Flux JavaScript assets to public/flux for static delivery (like livewire:publish --assets)';
public function handle(): int
{
$sourceDir = $this->resolveSourceDirectory();
$targetDir = public_path('flux');
if (! File::isDirectory($targetDir)) {
File::makeDirectory($targetDir, 0755, true);
}
$files = [
'flux.js',
'flux.min.js',
'editor.js',
'editor.min.js',
'editor.css',
];
$published = 0;
foreach ($files as $file) {
$source = $sourceDir.'/'.$file;
if (! is_file($source)) {
continue;
}
$target = $targetDir.'/'.$file;
if (is_file($target) && ! $this->option('force') && hash_file('sha256', $source) === hash_file('sha256', $target)) {
$this->line(" <comment>•</comment> {$file} (unverändert)");
continue;
}
File::copy($source, $target);
$this->line(" <info>✓</info> {$file}");
$published++;
}
if ($published === 0) {
$this->info('Flux-Assets sind bereits aktuell.');
return self::SUCCESS;
}
$this->newLine();
$this->info("{$published} Flux-Asset(s) nach public/flux/ veröffentlicht.");
return self::SUCCESS;
}
private function resolveSourceDirectory(): string
{
$proDist = base_path('vendor/livewire/flux-pro/dist');
if (is_dir($proDist)) {
return $proDist;
}
$freeDist = base_path('vendor/livewire/flux/dist');
if (is_dir($freeDist)) {
return $freeDist;
}
throw new RuntimeException('Flux vendor assets not found. Run composer install first.');
}
}

View file

@ -6,6 +6,7 @@ use App\Enums\DisplayVersionType;
use App\Models\DisplayMedia;
use App\Models\DisplayVersion;
use App\Models\DisplayVersionItem;
use App\Services\DisplayQrCodeService;
use App\Support\DisplayModuleSettings;
use Livewire\Attributes\On;
use Livewire\Component;
@ -174,6 +175,7 @@ class DisplayVersionEditor extends Component
public function saveSettings(): void
{
$this->normalizeBrandPositions();
$this->settings = $this->settingsWithQrImage($this->settings);
$this->version->update(['settings' => $this->settings]);
$this->showSettingsModal = false;
$this->refreshModulePreview();
@ -463,7 +465,7 @@ class DisplayVersionEditor extends Component
*/
private function buildItemContent(): array
{
return match ($this->itemType) {
$content = match ($this->itemType) {
'video' => [
'filename' => $this->videoFilename,
'title' => $this->videoTitle,
@ -514,6 +516,95 @@ class DisplayVersionEditor extends Component
],
default => [],
};
return $this->contentWithQrImage($content);
}
/**
* @param array<string, mixed> $content
* @return array<string, mixed>
*/
private function contentWithQrImage(array $content): array
{
$qrUrl = match ($this->itemType) {
'footer' => $content['url'] ?? null,
'slide' => $content['qr_url'] ?? null,
default => null,
};
$qrCodeService = app(DisplayQrCodeService::class);
$qrCode = $qrCodeService->generateForUrl(is_string($qrUrl) ? $qrUrl : null);
if ($qrCode) {
$content['qr_image_path'] = $qrCode['path'];
$content['qr_image_url'] = $qrCode['url'];
} elseif ($this->itemId && is_string($qrUrl) && $qrUrl !== '') {
$existingContent = DisplayVersionItem::find($this->itemId)?->content ?? [];
$existingQrUrl = match ($this->itemType) {
'footer' => $existingContent['url'] ?? null,
'slide' => $existingContent['qr_url'] ?? null,
default => null,
};
$preservedExistingQrImage = false;
if (is_string($existingQrUrl)
&& $qrCodeService->normalizeUrl($existingQrUrl) === $qrCodeService->normalizeUrl($qrUrl)
&& ! empty($existingContent['qr_image_path'])
&& ! empty($existingContent['qr_image_url'])) {
$content['qr_image_path'] = $existingContent['qr_image_path'];
$content['qr_image_url'] = $existingContent['qr_image_url'];
$preservedExistingQrImage = true;
}
if (! $preservedExistingQrImage) {
unset($content['qr_image_path'], $content['qr_image_url']);
}
} else {
unset($content['qr_image_path'], $content['qr_image_url']);
}
return $content;
}
/**
* @param array<string, mixed> $settings
* @return array<string, mixed>
*/
private function settingsWithQrImage(array $settings): array
{
if ($this->version->type !== DisplayVersionType::B2in) {
return $settings;
}
$qrUrl = $settings['qr_url'] ?? $settings['footer_url'] ?? null;
$qrCodeService = app(DisplayQrCodeService::class);
$qrCode = $qrCodeService->generateForUrl(is_string($qrUrl) ? $qrUrl : null);
if ($qrCode) {
$settings['qr_image_path'] = $qrCode['path'];
$settings['qr_image_url'] = $qrCode['url'];
} elseif (is_string($qrUrl) && $qrUrl !== '') {
$existingSettings = $this->settingsWithDefaults();
$existingQrUrl = $existingSettings['qr_url'] ?? $existingSettings['footer_url'] ?? null;
$preservedExistingQrImage = false;
if (is_string($existingQrUrl)
&& $qrCodeService->normalizeUrl($existingQrUrl) === $qrCodeService->normalizeUrl($qrUrl)
&& ! empty($existingSettings['qr_image_path'])
&& ! empty($existingSettings['qr_image_url'])) {
$settings['qr_image_path'] = $existingSettings['qr_image_path'];
$settings['qr_image_url'] = $existingSettings['qr_image_url'];
$preservedExistingQrImage = true;
}
if (! $preservedExistingQrImage) {
unset($settings['qr_image_path'], $settings['qr_image_url']);
}
} else {
unset($settings['qr_image_path'], $settings['qr_image_url']);
}
return $settings;
}
private function getActiveFlag(): bool

View file

@ -48,7 +48,7 @@ class QuickStatus extends Component
public function mount(?string $k = null): void
{
$validKey = config('domains.cabinet_status_key');
$key = $k ?? request()->query('k');
$key = $k ?? request()->query('k') ?? request()->query('key');
if (! $validKey || $key !== $validKey) {
$this->authorized = false;

View file

@ -90,6 +90,10 @@ class DisplayPlaylistConfigBuilder
$data['url'] = $item->content['url'];
}
if (! empty($item->content['qr_image_url'])) {
$data['qr_image_url'] = $item->content['qr_image_url'];
}
return $data;
});
@ -170,6 +174,7 @@ class DisplayPlaylistConfigBuilder
'disclaimer' => $content['disclaimer'] ?? '',
'show_disclaimer' => $content['show_disclaimer'] ?? ($content['disclaimer'] ?? '') !== '',
'qr_url' => $content['qr_url'] ?? '',
'qr_image_url' => $content['qr_image_url'] ?? '',
'qr_title' => $content['qr_title'] ?? '',
'show_qr' => $content['show_qr'] ?? ($content['qr_url'] ?? '') !== '',
'contact' => $content['contact'] ?? '',

View file

@ -0,0 +1,71 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
class DisplayQrCodeService
{
/**
* @return array{path: string, url: string}|null
*/
public function generateForUrl(?string $url): ?array
{
$normalizedUrl = $this->normalizeUrl($url);
if ($normalizedUrl === '') {
return null;
}
$path = 'display-qr-codes/'.sha1($normalizedUrl).'.png';
$disk = Storage::disk('public');
if (! $disk->exists($path)) {
try {
$response = Http::timeout(10)
->retry(2, 300)
->get($this->remoteQrUrl($normalizedUrl));
if (! $response->successful() || $response->body() === '') {
return null;
}
$disk->put($path, $response->body());
} catch (\Throwable) {
return null;
}
}
return [
'path' => $path,
'url' => asset('storage/'.$path),
];
}
public function normalizeUrl(?string $url): string
{
$url = trim((string) $url);
if ($url === '') {
return '';
}
if (str_starts_with($url, 'http://') || str_starts_with($url, 'https://')) {
return $url;
}
return 'https://'.$url;
}
private function remoteQrUrl(string $url): string
{
return 'https://api.qrserver.com/v1/create-qr-code/?'.http_build_query([
'size' => '300x300',
'margin' => '8',
'color' => '000000',
'bgcolor' => 'ffffff',
'data' => $url,
]);
}
}

View file

@ -19,7 +19,7 @@ class DisplayModuleSettings
],
DisplayVersionType::B2in->value => [
'theme' => 'dark',
'header_logo_url' => '../assets/b2in-logo-positive.svg',
'header_logo_url' => '/_cabinet/assets/b2in-logo-positive.svg',
'header_claim' => 'Connecting Design & Property',
'logo_position' => 'top-left',
'claim_position' => 'top-right',