added watermark settings tab on the branding page and added more package details to the billing page, added a new guest notifications page
This commit is contained in:
@@ -5,9 +5,8 @@ namespace App\Filament\SuperAdmin\Pages;
|
||||
use App\Models\WatermarkSetting;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class WatermarkSettingsPage extends Page
|
||||
{
|
||||
@@ -20,11 +19,19 @@ class WatermarkSettingsPage extends Page
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public ?string $asset = null;
|
||||
|
||||
public string $position = 'bottom-right';
|
||||
|
||||
public float $opacity = 0.25;
|
||||
|
||||
public float $scale = 0.2;
|
||||
|
||||
public int $padding = 16;
|
||||
|
||||
public int $offset_x = 0;
|
||||
|
||||
public int $offset_y = 0;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$settings = WatermarkSetting::query()->first();
|
||||
@@ -35,6 +42,8 @@ class WatermarkSettingsPage extends Page
|
||||
$this->opacity = (float) $settings->opacity;
|
||||
$this->scale = (float) $settings->scale;
|
||||
$this->padding = (int) $settings->padding;
|
||||
$this->offset_x = (int) ($settings->offset_x ?? 0);
|
||||
$this->offset_y = (int) ($settings->offset_y ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +89,20 @@ class WatermarkSettingsPage extends Page
|
||||
->minValue(0)
|
||||
->default(16)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('offset_x')
|
||||
->label('Offset X (px)')
|
||||
->numeric()
|
||||
->minValue(-500)
|
||||
->maxValue(500)
|
||||
->default(0)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('offset_y')
|
||||
->label('Offset Y (px)')
|
||||
->numeric()
|
||||
->minValue(-500)
|
||||
->maxValue(500)
|
||||
->default(0)
|
||||
->required(),
|
||||
])->columns(2);
|
||||
}
|
||||
|
||||
@@ -93,6 +116,8 @@ class WatermarkSettingsPage extends Page
|
||||
$settings->opacity = $this->opacity;
|
||||
$settings->scale = $this->scale;
|
||||
$settings->padding = $this->padding;
|
||||
$settings->offset_x = $this->offset_x;
|
||||
$settings->offset_y = $this->offset_y;
|
||||
$settings->save();
|
||||
|
||||
Notification::make()
|
||||
|
||||
@@ -21,6 +21,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
@@ -140,6 +141,7 @@ class EventController extends Controller
|
||||
}
|
||||
|
||||
$settings['branding_allowed'] = $package->branding_allowed !== false;
|
||||
$settings['watermark_allowed'] = $package->watermark_allowed !== false;
|
||||
|
||||
$eventData['settings'] = $settings;
|
||||
|
||||
@@ -258,7 +260,9 @@ class EventController extends Controller
|
||||
unset($validated[$unused]);
|
||||
}
|
||||
|
||||
$brandingAllowed = optional($event->eventPackage?->package)->branding_allowed !== false;
|
||||
$package = $event->eventPackage?->package;
|
||||
$brandingAllowed = optional($package)->branding_allowed !== false;
|
||||
$watermarkAllowed = optional($package)->watermark_allowed !== false;
|
||||
|
||||
if (isset($validated['settings']) && is_array($validated['settings'])) {
|
||||
$validated['settings'] = array_merge($event->settings ?? [], $validated['settings']);
|
||||
@@ -267,6 +271,92 @@ class EventController extends Controller
|
||||
}
|
||||
|
||||
$validated['settings']['branding_allowed'] = $brandingAllowed;
|
||||
$validated['settings']['watermark_allowed'] = $watermarkAllowed;
|
||||
|
||||
$settings = $validated['settings'];
|
||||
$watermark = Arr::get($settings, 'watermark', []);
|
||||
$existingWatermark = is_array($watermark) ? $watermark : [];
|
||||
|
||||
if (is_array($watermark)) {
|
||||
$mode = $watermark['mode'] ?? 'base';
|
||||
$policy = $watermarkAllowed ? 'basic' : 'none';
|
||||
|
||||
if (! $watermarkAllowed) {
|
||||
$mode = 'off';
|
||||
} elseif (! $brandingAllowed) {
|
||||
$mode = 'base';
|
||||
} elseif ($mode === 'off' && $policy === 'basic') {
|
||||
$mode = 'base';
|
||||
}
|
||||
|
||||
$assetPath = $watermark['asset'] ?? null;
|
||||
$assetDataUrl = $watermark['asset_data_url'] ?? null;
|
||||
|
||||
if (! $watermarkAllowed) {
|
||||
$assetPath = null;
|
||||
}
|
||||
|
||||
if ($assetDataUrl && $mode === 'custom' && $brandingAllowed) {
|
||||
if (! preg_match('/^data:image\\/(png|webp|jpe?g);base64,(.+)$/i', $assetDataUrl, $matches)) {
|
||||
throw ValidationException::withMessages([
|
||||
'settings.watermark.asset_data_url' => __('Ungültiges Wasserzeichen-Bild.'),
|
||||
]);
|
||||
}
|
||||
|
||||
$decoded = base64_decode($matches[2], true);
|
||||
|
||||
if ($decoded === false) {
|
||||
throw ValidationException::withMessages([
|
||||
'settings.watermark.asset_data_url' => __('Wasserzeichen konnte nicht gelesen werden.'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (strlen($decoded) > 3 * 1024 * 1024) { // 3 MB
|
||||
throw ValidationException::withMessages([
|
||||
'settings.watermark.asset_data_url' => __('Wasserzeichen ist zu groß (max. 3 MB).'),
|
||||
]);
|
||||
}
|
||||
|
||||
$extension = str_starts_with(strtolower($matches[1]), 'jp') ? 'jpg' : strtolower($matches[1]);
|
||||
$path = sprintf('branding/watermarks/event-%s.%s', $event->id, $extension);
|
||||
Storage::disk('public')->put($path, $decoded);
|
||||
$assetPath = $path;
|
||||
}
|
||||
|
||||
$position = $watermark['position'] ?? 'bottom-right';
|
||||
$validPositions = [
|
||||
'top-left',
|
||||
'top-center',
|
||||
'top-right',
|
||||
'middle-left',
|
||||
'center',
|
||||
'middle-right',
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
];
|
||||
|
||||
if (! in_array($position, $validPositions, true)) {
|
||||
$position = 'bottom-right';
|
||||
}
|
||||
|
||||
$settings['watermark'] = [
|
||||
'mode' => $mode,
|
||||
'asset' => $assetPath,
|
||||
'position' => $position,
|
||||
'opacity' => isset($watermark['opacity']) ? (float) $watermark['opacity'] : ($existingWatermark['opacity'] ?? null),
|
||||
'scale' => isset($watermark['scale']) ? (float) $watermark['scale'] : ($existingWatermark['scale'] ?? null),
|
||||
'padding' => isset($watermark['padding']) ? (int) $watermark['padding'] : ($existingWatermark['padding'] ?? null),
|
||||
'offset_x' => isset($watermark['offset_x']) ? (int) $watermark['offset_x'] : ($existingWatermark['offset_x'] ?? 0),
|
||||
'offset_y' => isset($watermark['offset_y']) ? (int) $watermark['offset_y'] : ($existingWatermark['offset_y'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
if (array_key_exists('watermark_serve_originals', $settings)) {
|
||||
$settings['watermark_serve_originals'] = (bool) $settings['watermark_serve_originals'];
|
||||
}
|
||||
|
||||
$validated['settings'] = $settings;
|
||||
|
||||
$event->update($validated);
|
||||
$event->load(['eventType', 'tenant']);
|
||||
|
||||
@@ -30,8 +30,16 @@ class TenantPackageController extends Controller
|
||||
->get();
|
||||
|
||||
$packages->each(function ($package) {
|
||||
$package->remaining_events = $package->package->max_events_per_year - $package->used_events;
|
||||
$package->package_limits = $package->package->getAttributes(); // Or custom accessor for limits
|
||||
$pkg = $package->package;
|
||||
$package->remaining_events = $pkg->max_events_per_year - $package->used_events;
|
||||
$package->package_limits = array_merge(
|
||||
$pkg->limits,
|
||||
[
|
||||
'branding_allowed' => $pkg->branding_allowed,
|
||||
'watermark_allowed' => $pkg->watermark_allowed,
|
||||
'features' => $pkg->features,
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -46,6 +46,27 @@ class EventStoreRequest extends FormRequest
|
||||
'settings.branding.*' => ['nullable'],
|
||||
'settings.engagement_mode' => ['nullable', Rule::in(['tasks', 'photo_only'])],
|
||||
'settings.guest_upload_visibility' => ['nullable', Rule::in(['review', 'immediate'])],
|
||||
'settings.watermark' => ['nullable', 'array'],
|
||||
'settings.watermark.mode' => ['nullable', Rule::in(['base', 'custom', 'off'])],
|
||||
'settings.watermark.asset' => ['nullable', 'string', 'max:500'],
|
||||
'settings.watermark.asset_data_url' => ['nullable', 'string'],
|
||||
'settings.watermark.position' => ['nullable', Rule::in([
|
||||
'top-left',
|
||||
'top-center',
|
||||
'top-right',
|
||||
'middle-left',
|
||||
'center',
|
||||
'middle-right',
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
])],
|
||||
'settings.watermark.opacity' => ['nullable', 'numeric', 'min:0', 'max:1'],
|
||||
'settings.watermark.scale' => ['nullable', 'numeric', 'min:0.05', 'max:1'],
|
||||
'settings.watermark.padding' => ['nullable', 'integer', 'min:0', 'max:500'],
|
||||
'settings.watermark.offset_x' => ['nullable', 'integer', 'min:-500', 'max:500'],
|
||||
'settings.watermark.offset_y' => ['nullable', 'integer', 'min:-500', 'max:500'],
|
||||
'settings.watermark_serve_originals' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ class EventResource extends JsonResource
|
||||
'price' => $eventPackage->purchased_price,
|
||||
'purchased_at' => $eventPackage->purchased_at?->toIso8601String(),
|
||||
'expires_at' => $eventPackage->gallery_expires_at?->toIso8601String(),
|
||||
'branding_allowed' => (bool) optional($eventPackage->package)->branding_allowed,
|
||||
'watermark_allowed' => (bool) optional($eventPackage->package)->watermark_allowed,
|
||||
] : null,
|
||||
'limits' => $eventPackage && $limitEvaluator
|
||||
? $limitEvaluator->summarizeEventPackage($eventPackage)
|
||||
|
||||
@@ -15,5 +15,7 @@ class WatermarkSetting extends Model
|
||||
'opacity',
|
||||
'scale',
|
||||
'padding',
|
||||
'offset_x',
|
||||
'offset_y',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class ImageHelper
|
||||
$h = imagesy($src);
|
||||
if ($w === 0 || $h === 0) {
|
||||
imagedestroy($src);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -138,18 +139,44 @@ class ImageHelper
|
||||
$x = $padding;
|
||||
$y = $padding;
|
||||
|
||||
if ($position === 'top-right') {
|
||||
$x = max(0, $srcW - $targetW - $padding);
|
||||
} elseif ($position === 'bottom-left') {
|
||||
$y = max(0, $srcH - $targetH - $padding);
|
||||
} elseif ($position === 'bottom-right') {
|
||||
$x = max(0, $srcW - $targetW - $padding);
|
||||
$y = max(0, $srcH - $targetH - $padding);
|
||||
} elseif ($position === 'center') {
|
||||
$x = (int) max(0, ($srcW - $targetW) / 2);
|
||||
$y = (int) max(0, ($srcH - $targetH) / 2);
|
||||
switch ($position) {
|
||||
case 'top-right':
|
||||
$x = max(0, $srcW - $targetW - $padding);
|
||||
break;
|
||||
case 'top-center':
|
||||
$x = (int) max(0, ($srcW - $targetW) / 2);
|
||||
break;
|
||||
case 'middle-left':
|
||||
$y = (int) max(0, ($srcH - $targetH) / 2);
|
||||
break;
|
||||
case 'center':
|
||||
$x = (int) max(0, ($srcW - $targetW) / 2);
|
||||
$y = (int) max(0, ($srcH - $targetH) / 2);
|
||||
break;
|
||||
case 'middle-right':
|
||||
$x = max(0, $srcW - $targetW - $padding);
|
||||
$y = (int) max(0, ($srcH - $targetH) / 2);
|
||||
break;
|
||||
case 'bottom-left':
|
||||
$y = max(0, $srcH - $targetH - $padding);
|
||||
break;
|
||||
case 'bottom-center':
|
||||
$x = (int) max(0, ($srcW - $targetW) / 2);
|
||||
$y = max(0, $srcH - $targetH - $padding);
|
||||
break;
|
||||
case 'bottom-right':
|
||||
$x = max(0, $srcW - $targetW - $padding);
|
||||
$y = max(0, $srcH - $targetH - $padding);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$offsetX = (int) ($config['offset_x'] ?? 0);
|
||||
$offsetY = (int) ($config['offset_y'] ?? 0);
|
||||
$x = max(0, min($srcW - $targetW, $x + $offsetX));
|
||||
$y = max(0, min($srcH - $targetH, $y + $offsetY));
|
||||
|
||||
$opacity = max(0.0, min(1.0, (float) ($config['opacity'] ?? 0.25)));
|
||||
$mergeOpacity = (int) round($opacity * 100); // imagecopymerge uses 0-100
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class WatermarkConfigResolver
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{type:string, policy:string, asset?:string, position?:string, opacity?:float, scale?:float, padding?:int}
|
||||
* @return array{type:string, policy:string, asset?:string, position?:string, opacity?:float, scale?:float, padding?:int, offset_x?:int, offset_y?:int}
|
||||
*/
|
||||
public static function resolve(Event $event): array
|
||||
{
|
||||
@@ -61,6 +61,8 @@ class WatermarkConfigResolver
|
||||
'opacity' => $baseSetting?->opacity ?? config('watermark.base.opacity', 0.25),
|
||||
'scale' => $baseSetting?->scale ?? config('watermark.base.scale', 0.2),
|
||||
'padding' => $baseSetting?->padding ?? config('watermark.base.padding', 16),
|
||||
'offset_x' => $baseSetting?->offset_x ?? config('watermark.base.offset_x', 0),
|
||||
'offset_y' => $baseSetting?->offset_y ?? config('watermark.base.offset_y', 0),
|
||||
];
|
||||
|
||||
$event->loadMissing('eventPackage.package', 'tenant');
|
||||
@@ -91,6 +93,8 @@ class WatermarkConfigResolver
|
||||
$opacity = (float) ($source['opacity'] ?? $base['opacity'] ?? 0.25);
|
||||
$scale = (float) ($source['scale'] ?? $base['scale'] ?? 0.2);
|
||||
$padding = (int) ($source['padding'] ?? $base['padding'] ?? 16);
|
||||
$offsetX = (int) ($source['offset_x'] ?? $base['offset_x'] ?? 0);
|
||||
$offsetY = (int) ($source['offset_y'] ?? $base['offset_y'] ?? 0);
|
||||
|
||||
$clamp = static fn (float $value, float $min, float $max) => max($min, min($max, $value));
|
||||
|
||||
@@ -102,6 +106,8 @@ class WatermarkConfigResolver
|
||||
'opacity' => $clamp($opacity, 0.0, 1.0),
|
||||
'scale' => $clamp($scale, 0.05, 1.0),
|
||||
'padding' => max(0, $padding),
|
||||
'offset_x' => max(-500, min(500, $offsetX)),
|
||||
'offset_y' => max(-500, min(500, $offsetY)),
|
||||
'serve_originals' => $serveOriginals,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user