diff --git a/CHANGELOG.md b/CHANGELOG.md index a2cde56..3fec243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## [1.5.0] - 2026-02-22 + +### Added +- Configurable background frame for printed card PDFs: supervisors set a system-wide default, users can upload a personal override in their profile, with automatic fallback to the built-in card back image + ## [1.4.0] - 2026-02-21 ### Added diff --git a/app/Filament/Pages/PdfSettings.php b/app/Filament/Pages/PdfSettings.php new file mode 100644 index 0000000..5f44699 --- /dev/null +++ b/app/Filament/Pages/PdfSettings.php @@ -0,0 +1,139 @@ +check() && auth()->user()->isSupervisor(); + } + + /** + * Load current settings into the form. + */ + public function mount(): void + { + $this->form->fill([ + 'card_pdf_background' => WebsiteSetting::get('card_pdf_background'), + 'card_pdf_overlay' => WebsiteSetting::get('card_pdf_overlay', 'dark'), + ]); + } + + /** + * Define the settings form. + */ + public function form(Form $form): Form + { + return $form + ->schema([ + Section::make('Card Print Background') + ->description('Upload a default background image applied to all printed card PDFs. Users may override this with their own background in their profile.') + ->icon('heroicon-o-photo') + ->schema([ + FileUpload::make('card_pdf_background') + ->label('Default Card Background Image') + ->disk('public') + ->directory('pdf-backgrounds') + ->image() + ->imageEditor() + ->maxSize(5120) + ->helperText('Recommended: match your card dimensions. Falls back to the built-in card back image if not set.') + ->columnSpanFull(), + Select::make('card_pdf_overlay') + ->label('Default Overlay Style') + ->options([ + 'dark' => 'Dark — dark overlay, light text (best for light backgrounds)', + 'light' => 'Light — light overlay, dark text (best for light backgrounds)', + ]) + ->default('dark') + ->required() + ->helperText('Controls the overlay tint and text colours applied over the card background. Decks can override this individually.') + ->columnSpanFull(), + ]), + ]) + ->statePath('data'); + } + + /** + * Save the settings to the database. + */ + public function save(): void + { + $data = $this->form->getState(); + + WebsiteSetting::updateOrCreate( + ['key' => 'card_pdf_background'], + [ + 'value' => $data['card_pdf_background'] ?? null, + 'type' => 'text', + 'group' => 'general', + 'label' => 'Card PDF Background', + 'description' => 'System-wide default background image for printed card PDFs.', + 'order' => 1, + ] + ); + + WebsiteSetting::updateOrCreate( + ['key' => 'card_pdf_overlay'], + [ + 'value' => $data['card_pdf_overlay'] ?? 'dark', + 'type' => 'text', + 'group' => 'general', + 'label' => 'Card PDF Overlay Style', + 'description' => 'System-wide default overlay style (dark or light) for printed card PDFs.', + 'order' => 2, + ] + ); + + Notification::make() + ->title('PDF settings saved successfully.') + ->success() + ->send(); + } + + /** + * Get the header actions. + */ + protected function getHeaderActions(): array + { + return [ + Action::make('save') + ->label('Save Settings') + ->icon('heroicon-o-check') + ->color('primary') + ->action('save'), + ]; + } +} diff --git a/app/Filament/Resources/DeckResource.php b/app/Filament/Resources/DeckResource.php index 1429452..3b1475f 100644 --- a/app/Filament/Resources/DeckResource.php +++ b/app/Filament/Resources/DeckResource.php @@ -8,6 +8,8 @@ use App\Models\Deck; use App\Models\Game; use Filament\Forms; +use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Select; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; @@ -83,6 +85,30 @@ public static function form(Form $form): Form ->collapsible(), ]) ->columns(2), + Forms\Components\Section::make('Print Settings') + ->description('Customize how this deck looks when printed as a PDF') + ->schema([ + FileUpload::make('pdf_background') + ->label('Card Background Image') + ->disk('public') + ->directory('pdf-backgrounds') + ->image() + ->imageEditor() + ->maxSize(5120) + ->helperText('Overrides the system default. Appears behind each card illustration in the printed PDF.') + ->columnSpanFull(), + Select::make('pdf_overlay') + ->label('Overlay Style') + ->options([ + 'dark' => 'Dark — dark overlay, light text', + 'light' => 'Light — light overlay, dark text', + ]) + ->placeholder('Use system default') + ->helperText('Overrides the system default overlay style for this deck only.') + ->columnSpanFull(), + ]) + ->collapsible(), + Forms\Components\Section::make('Cards in Deck') ->description('There is the list of cards on this deck') ->schema([ diff --git a/app/Models/Deck.php b/app/Models/Deck.php index a378e7f..ca7ebb1 100644 --- a/app/Models/Deck.php +++ b/app/Models/Deck.php @@ -22,6 +22,8 @@ class Deck extends Model 'deck_name', 'deck_description', 'deck_data', + 'pdf_background', + 'pdf_overlay', ]; public function game() diff --git a/app/Services/DeckPdfService.php b/app/Services/DeckPdfService.php index 5a77dc0..5d526ba 100644 --- a/app/Services/DeckPdfService.php +++ b/app/Services/DeckPdfService.php @@ -7,6 +7,7 @@ use App\Models\Card; use App\Models\Deck; +use App\Models\WebsiteSetting; use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Support\Facades\Storage; @@ -61,19 +62,38 @@ public function expandCards(Deck $deck): array } /** - * Resolve the filesystem path for a card image, falling back to the placeholder. + * Resolve the background image for the card layer: + * - If the card has its own illustration, use that. + * - Otherwise fall through: deck override → system default → built-in placeholder. */ - protected function resolveImagePath(Card $card): string + protected function resolveImagePath(Card $card, Deck $deck): string { - $placeholder = public_path('images/cardsforge-back.png'); + if (!empty($card->image)) { + $path = Storage::disk('public')->path($card->image); + if (file_exists($path)) { + return $path; + } + } - if (empty($card->image)) { - return $placeholder; + // 1. Deck override + if (!empty($deck->pdf_background)) { + $path = Storage::disk('public')->path($deck->pdf_background); + if (file_exists($path)) { + return $path; + } } - $path = Storage::disk('public')->path($card->image); + // 2. System default + $setting = WebsiteSetting::get('card_pdf_background'); + if (!empty($setting)) { + $path = Storage::disk('public')->path($setting); + if (file_exists($path)) { + return $path; + } + } - return file_exists($path) ? $path : $placeholder; + // 3. Built-in fallback + return public_path('images/cardsforge-back.png'); } /** @@ -88,13 +108,17 @@ public function download(Deck $deck): \Symfony\Component\HttpFoundation\Streamed $cardWidth = $deck->game->card_width_mm ?? 63.5; $cardHeight = $deck->game->card_height_mm ?? 88.9; + // Resolve overlay mode: deck → system default → 'dark' + $overlayMode = $deck->pdf_overlay + ?? WebsiteSetting::get('card_pdf_overlay', 'dark'); + $pagesData = []; foreach ($pages as $pageCards) { $pageItems = []; foreach ($pageCards as $card) { $pageItems[] = [ 'card' => $card, - 'imagePath' => $this->resolveImagePath($card), + 'imagePath' => $this->resolveImagePath($card, $deck), ]; } $pagesData[] = $pageItems; @@ -105,6 +129,7 @@ public function download(Deck $deck): \Symfony\Component\HttpFoundation\Streamed 'pages' => $pagesData, 'cardWidth' => $cardWidth, 'cardHeight' => $cardHeight, + 'overlayMode' => $overlayMode, ]) ->setPaper('a4', 'portrait') ->setOption('isHtml5ParserEnabled', true) diff --git a/database/migrations/2026_02_22_000001_add_pdf_background_to_users_table.php b/database/migrations/2026_02_22_000001_add_pdf_background_to_users_table.php new file mode 100644 index 0000000..18ef7da --- /dev/null +++ b/database/migrations/2026_02_22_000001_add_pdf_background_to_users_table.php @@ -0,0 +1,25 @@ +string('pdf_background')->nullable()->after('avatar_url'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('pdf_background'); + }); + } +}; diff --git a/database/migrations/2026_02_22_000002_move_pdf_background_from_users_to_decks_table.php b/database/migrations/2026_02_22_000002_move_pdf_background_from_users_to_decks_table.php new file mode 100644 index 0000000..ccad342 --- /dev/null +++ b/database/migrations/2026_02_22_000002_move_pdf_background_from_users_to_decks_table.php @@ -0,0 +1,33 @@ +string('pdf_background')->nullable()->after('deck_description'); + }); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('pdf_background'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('pdf_background')->nullable()->after('avatar_url'); + }); + + Schema::table('decks', function (Blueprint $table) { + $table->dropColumn('pdf_background'); + }); + } +}; diff --git a/database/migrations/2026_02_22_000003_add_pdf_overlay_to_decks_table.php b/database/migrations/2026_02_22_000003_add_pdf_overlay_to_decks_table.php new file mode 100644 index 0000000..7dc3201 --- /dev/null +++ b/database/migrations/2026_02_22_000003_add_pdf_overlay_to_decks_table.php @@ -0,0 +1,25 @@ +string('pdf_overlay')->nullable()->default(null)->after('pdf_background'); + }); + } + + public function down(): void + { + Schema::table('decks', function (Blueprint $table) { + $table->dropColumn('pdf_overlay'); + }); + } +}; diff --git a/resources/views/filament/pages/pdf-settings.blade.php b/resources/views/filament/pages/pdf-settings.blade.php new file mode 100644 index 0000000..d45ede8 --- /dev/null +++ b/resources/views/filament/pages/pdf-settings.blade.php @@ -0,0 +1,5 @@ + + + {{ $this->form }} + + diff --git a/resources/views/pdf/deck-print.blade.php b/resources/views/pdf/deck-print.blade.php index 7a1026c..40f32fb 100644 --- a/resources/views/pdf/deck-print.blade.php +++ b/resources/views/pdf/deck-print.blade.php @@ -61,7 +61,7 @@ border: 1px solid #000; } - /* Background image fills entire card */ + /* Card background image (illustration or configured background) */ .card-bg { position: absolute; top: 0; @@ -76,14 +76,18 @@ display: block; } - /* Semi-transparent overlay to dim the background */ + /* Semi-transparent overlay */ .card-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; + @if(($overlayMode ?? 'dark') === 'light') + background: rgba(255, 255, 255, 0.70); + @else background: rgba(0, 0, 0, 0.70); + @endif } /* Foreground text layer */ @@ -97,6 +101,48 @@ display: block; } + @if(($overlayMode ?? 'dark') === 'light') + .card-name { + font-size: 9.1pt; + font-weight: bold; + color: #111111; + text-align: center; + background: rgba(255, 255, 255, 0.60); + padding: 1mm 2mm; + margin-bottom: 1.5mm; + } + + .card-type { + font-size: 7.2pt; + color: #333333; + text-align: center; + margin-bottom: 2mm; + } + + .card-text { + font-size: 6.5pt; + color: #222222; + text-align: left; + margin-bottom: 2mm; + line-height: 1.3; + } + + .card-fields { + margin-top: 1mm; + } + + .card-field { + font-size: 6.5pt; + color: #111111; + margin-bottom: 0.8mm; + line-height: 1.2; + } + + .card-field-name { + font-weight: bold; + color: #8B4513; + } + @else .card-name { font-size: 9.1pt; font-weight: bold; @@ -137,6 +183,7 @@ font-weight: bold; color: #ffd700; } + @endif /* Crop marks */ .crop-mark { @@ -176,7 +223,7 @@ @endphp
- {{-- Background image --}} + {{-- Card background (illustration, or configured background/fallback) --}}
{{ $card->name }}