Blendet den B2IN-Text-Verlauf ohne Footer aus, zeigt 9:16-Format-Badges in der Mediathek, normalisiert Modul-Booleans und korrigiert Flux-Switches im Dark Mode. Co-authored-by: Cursor <cursoragent@cursor.com>
234 lines
6 KiB
PHP
234 lines
6 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
class DisplayMedia extends Model
|
|
{
|
|
/** @use HasFactory<\Database\Factories\DisplayMediaFactory> */
|
|
use HasFactory;
|
|
|
|
private const DISPLAY_TARGET_RATIO = 9 / 16;
|
|
|
|
private const DISPLAY_RATIO_TOLERANCE = 0.03;
|
|
|
|
protected $table = 'display_media';
|
|
|
|
protected $fillable = [
|
|
'filename',
|
|
'disk',
|
|
'path',
|
|
'external_url',
|
|
'source_type',
|
|
'type',
|
|
'mime_type',
|
|
'file_size',
|
|
'thumbnail_path',
|
|
'alt_text',
|
|
'title',
|
|
'collection',
|
|
'metadata',
|
|
'is_active',
|
|
];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'metadata' => 'array',
|
|
'file_size' => 'integer',
|
|
'is_active' => 'boolean',
|
|
];
|
|
}
|
|
|
|
// ========================================
|
|
// ACCESSORS
|
|
// ========================================
|
|
|
|
public function getUrl(): string
|
|
{
|
|
if ($this->isExternal()) {
|
|
return $this->external_url;
|
|
}
|
|
|
|
return Storage::url($this->path);
|
|
}
|
|
|
|
public function getThumbnailUrl(): ?string
|
|
{
|
|
if ($this->thumbnail_path) {
|
|
return Storage::url($this->thumbnail_path);
|
|
}
|
|
|
|
if ($this->isUpload() && $this->isImage()) {
|
|
return $this->getUrl();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array{status: 'match'|'mismatch'|'unknown', label: string, color: string, description: string}
|
|
*/
|
|
public function getDisplayFormatStatus(): array
|
|
{
|
|
$dimensions = $this->getDimensions();
|
|
|
|
if ($dimensions === null) {
|
|
return [
|
|
'status' => 'unknown',
|
|
'label' => 'Format unbekannt',
|
|
'color' => 'zinc',
|
|
'description' => 'Keine Abmessungen gespeichert. Ziel ist 9:16 im Hochformat.',
|
|
];
|
|
}
|
|
|
|
$ratio = $dimensions['width'] / $dimensions['height'];
|
|
$matchesTarget = abs($ratio - self::DISPLAY_TARGET_RATIO) <= self::DISPLAY_TARGET_RATIO * self::DISPLAY_RATIO_TOLERANCE;
|
|
$description = sprintf(
|
|
'%dx%d px, erkanntes Verhältnis %s. Ziel ist 9:16 im Hochformat.',
|
|
$dimensions['width'],
|
|
$dimensions['height'],
|
|
$this->formatRatio($dimensions['width'], $dimensions['height']),
|
|
);
|
|
|
|
return [
|
|
'status' => $matchesTarget ? 'match' : 'mismatch',
|
|
'label' => $matchesTarget ? 'Format passt' : 'Format passt nicht',
|
|
'color' => $matchesTarget ? 'green' : 'amber',
|
|
'description' => $description,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{width: int, height: int}|null
|
|
*/
|
|
public function getDimensions(): ?array
|
|
{
|
|
$width = $this->metadata['width'] ?? null;
|
|
$height = $this->metadata['height'] ?? null;
|
|
|
|
if (! is_numeric($width) || ! is_numeric($height) || (int) $width <= 0 || (int) $height <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'width' => (int) $width,
|
|
'height' => (int) $height,
|
|
];
|
|
}
|
|
|
|
// ========================================
|
|
// TYPE CHECKS
|
|
// ========================================
|
|
|
|
public function isUpload(): bool
|
|
{
|
|
return $this->source_type === 'upload';
|
|
}
|
|
|
|
public function isExternal(): bool
|
|
{
|
|
return $this->source_type === 'external';
|
|
}
|
|
|
|
public function isImage(): bool
|
|
{
|
|
return $this->type === 'image';
|
|
}
|
|
|
|
public function isVideo(): bool
|
|
{
|
|
return $this->type === 'video';
|
|
}
|
|
|
|
// ========================================
|
|
// DISPLAY HELPERS
|
|
// ========================================
|
|
|
|
public function getHumanFileSize(): string
|
|
{
|
|
if ($this->file_size === 0) {
|
|
return $this->isExternal() ? 'Extern' : '0 B';
|
|
}
|
|
|
|
$units = ['B', 'KB', 'MB', 'GB'];
|
|
$size = $this->file_size;
|
|
$unitIndex = 0;
|
|
|
|
while ($size >= 1024 && $unitIndex < count($units) - 1) {
|
|
$size /= 1024;
|
|
$unitIndex++;
|
|
}
|
|
|
|
return round($size, 1).' '.$units[$unitIndex];
|
|
}
|
|
|
|
public function getDisplayName(): string
|
|
{
|
|
return $this->title ?: $this->filename;
|
|
}
|
|
|
|
private function formatRatio(int $width, int $height): string
|
|
{
|
|
$divisor = $this->greatestCommonDivisor($width, $height);
|
|
|
|
return ($width / $divisor).':'.($height / $divisor);
|
|
}
|
|
|
|
private function greatestCommonDivisor(int $first, int $second): int
|
|
{
|
|
while ($second !== 0) {
|
|
$remainder = $first % $second;
|
|
$first = $second;
|
|
$second = $remainder;
|
|
}
|
|
|
|
return max($first, 1);
|
|
}
|
|
|
|
// ========================================
|
|
// SCOPES
|
|
// ========================================
|
|
|
|
public function scopeImages(Builder $query): Builder
|
|
{
|
|
return $query->where('type', 'image');
|
|
}
|
|
|
|
public function scopeVideos(Builder $query): Builder
|
|
{
|
|
return $query->where('type', 'video');
|
|
}
|
|
|
|
public function scopeUploads(Builder $query): Builder
|
|
{
|
|
return $query->where('source_type', 'upload');
|
|
}
|
|
|
|
public function scopeExternals(Builder $query): Builder
|
|
{
|
|
return $query->where('source_type', 'external');
|
|
}
|
|
|
|
public function scopeActive(Builder $query): Builder
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
public function scopeInCollection(Builder $query, string $collection): Builder
|
|
{
|
|
return $query->where('collection', $collection);
|
|
}
|
|
|
|
public function scopeSearch(Builder $query, string $term): Builder
|
|
{
|
|
return $query->where(function (Builder $query) use ($term): void {
|
|
$query->where('filename', 'like', "%{$term}%")
|
|
->orWhere('title', 'like', "%{$term}%");
|
|
});
|
|
}
|
|
}
|