diff --git a/app/Jobs/ConvertToComic.php b/app/Jobs/ConvertToComic.php index 3dce2b9..147f7a3 100644 --- a/app/Jobs/ConvertToComic.php +++ b/app/Jobs/ConvertToComic.php @@ -20,9 +20,12 @@ class ConvertToComic implements ShouldQueue { $absolutePath = Storage::disk('public')->path($this->photoJob->image_path); + $gmicOps = $this->photoJob->setting?->gmic_command + ?? 'cl_comic 4,1,0,0,1,15,15,1,10,20,6,2,0,0,0,0,0,0,50,50'; + $result = Process::timeout(0)->run( 'gmic ' . escapeshellarg($absolutePath) . - ' cl_comic 4,1,0,0,1,15,15,1,10,20,6,2,0,0,0,0,0,0,50,50' . + ' ' . $gmicOps . ' -o ' . escapeshellarg($absolutePath) ); diff --git a/app/Livewire/CameraCapture.php b/app/Livewire/CameraCapture.php index 779202f..6f70e26 100644 --- a/app/Livewire/CameraCapture.php +++ b/app/Livewire/CameraCapture.php @@ -4,6 +4,8 @@ namespace App\Livewire; use App\Jobs\ConvertToComic; use App\Models\PhotoJob; +use App\Models\PhotoSession; +use App\Models\PhotoSetting; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Livewire\Component; @@ -12,17 +14,6 @@ class CameraCapture extends Component { public string $status = ''; - public function delete(int $jobId): void - { - $job = PhotoJob::find($jobId); - if (!$job) return; - - Storage::disk('public')->delete($job->image_path); - $job->delete(); - - $this->status = "Job #{$jobId} deleted"; - } - public function capture(string $imageData): void { if (!preg_match('/^data:image\/(\w+);base64,/', $imageData, $type)) { @@ -31,31 +22,66 @@ class CameraCapture extends Component } $extension = strtolower($type[1]); - $filename = 'photos/' . Str::uuid() . '.' . $extension; $raw = base64_decode(substr($imageData, strpos($imageData, ',') + 1)); - Storage::disk('public')->put($filename, $raw); + $originalPath = 'photos/' . Str::uuid() . '.' . $extension; + Storage::disk('public')->put($originalPath, $raw); - $job = PhotoJob::create([ - 'image_path' => $filename, - 'status' => 'processing', - ]); + $session = PhotoSession::create(['original_image_path' => $originalPath]); - ConvertToComic::dispatch($job); + $settings = PhotoSetting::active()->get(); - $this->status = "Processing photo…"; + foreach ($settings as $setting) { + $variantPath = 'photos/' . Str::uuid() . '.' . $extension; + Storage::disk('public')->put($variantPath, $raw); + + $job = PhotoJob::create([ + 'image_path' => $variantPath, + 'status' => 'processing', + 'photo_session_id' => $session->id, + 'photo_setting_id' => $setting->id, + ]); + + ConvertToComic::dispatch($job); + } + + $this->status = count($settings) . ' Varianten werden erstellt…'; + } + + public function selectVariant(int $sessionId, int $jobId): void + { + PhotoSession::where('id', $sessionId)->update(['selected_job_id' => $jobId]); + } + + public function deleteSession(int $sessionId): void + { + $session = PhotoSession::with('jobs')->find($sessionId); + if (!$session) return; + + foreach ($session->jobs as $job) { + Storage::disk('public')->delete($job->image_path); + $job->delete(); + } + + Storage::disk('public')->delete($session->original_image_path); + $session->delete(); + + $this->status = "Session #{$sessionId} gelöscht"; } public function render() { - $recentPhotos = PhotoJob::latest()->take(10)->get()->map(fn($job) => [ - 'id' => $job->id, - 'url' => Storage::disk('public')->url($job->image_path) . '?v=' . $job->updated_at->timestamp, - 'status' => $job->status, - ]); + $sessions = PhotoSession::latest()->take(10)->get(); - $hasProcessing = $recentPhotos->contains('status', 'processing'); + foreach ($sessions as $session) { + $session->setRelation('jobs', $session->jobs()->with('setting')->orderBy('photo_setting_id')->get()); + } - return view('livewire.camera-capture', compact('recentPhotos', 'hasProcessing')); + $currentSession = $sessions->first(); + $previousSessions = $sessions->skip(1); + + $hasProcessing = $currentSession && $currentSession->jobs->contains('status', 'processing'); + + return view('livewire.camera-capture', compact('currentSession', 'previousSessions', 'hasProcessing')); } } diff --git a/app/Livewire/PhotoSettings.php b/app/Livewire/PhotoSettings.php new file mode 100644 index 0000000..399de88 --- /dev/null +++ b/app/Livewire/PhotoSettings.php @@ -0,0 +1,75 @@ +update(['is_active' => !$setting->is_active]); + } + + public function startEdit(int $id, string $name, string $command): void + { + $this->editingId = [$id => true]; + $this->editName[$id] = $name; + $this->editCommand[$id] = $command; + } + + public function saveEdit(int $id): void + { + PhotoSetting::findOrFail($id)->update([ + 'name' => trim($this->editName[$id] ?? ''), + 'gmic_command' => trim($this->editCommand[$id] ?? ''), + ]); + unset($this->editingId[$id]); + } + + public function cancelEdit(int $id): void + { + unset($this->editingId[$id]); + } + + public function addSetting(): void + { + $name = trim($this->newName); + $command = trim($this->newCommand); + + if (!$name || !$command) return; + + $max = PhotoSetting::max('sort_order') ?? 0; + PhotoSetting::create([ + 'name' => $name, + 'gmic_command' => $command, + 'sort_order' => $max + 1, + 'is_active' => true, + ]); + + $this->newName = ''; + $this->newCommand = ''; + $this->showForm = false; + } + + public function delete(int $id): void + { + PhotoSetting::findOrFail($id)->delete(); + } + + public function render() + { + return view('livewire.photo-settings', [ + 'settings' => PhotoSetting::orderBy('sort_order')->get(), + ]); + } +} diff --git a/app/Models/PhotoJob.php b/app/Models/PhotoJob.php index 7df7754..b4687ba 100644 --- a/app/Models/PhotoJob.php +++ b/app/Models/PhotoJob.php @@ -3,8 +3,19 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class PhotoJob extends Model { - protected $fillable = ['image_path', 'status']; + protected $fillable = ['image_path', 'status', 'photo_session_id', 'photo_setting_id']; + + public function session(): BelongsTo + { + return $this->belongsTo(PhotoSession::class, 'photo_session_id'); + } + + public function setting(): BelongsTo + { + return $this->belongsTo(PhotoSetting::class, 'photo_setting_id'); + } } diff --git a/app/Models/PhotoSession.php b/app/Models/PhotoSession.php new file mode 100644 index 0000000..e5cc0ba --- /dev/null +++ b/app/Models/PhotoSession.php @@ -0,0 +1,22 @@ +hasMany(PhotoJob::class)->with('setting')->orderBy('photo_setting_id'); + } + + public function selectedJob(): BelongsTo + { + return $this->belongsTo(PhotoJob::class, 'selected_job_id'); + } +} diff --git a/app/Models/PhotoSetting.php b/app/Models/PhotoSetting.php new file mode 100644 index 0000000..c6f6d6f --- /dev/null +++ b/app/Models/PhotoSetting.php @@ -0,0 +1,23 @@ + 'boolean']; + + public function jobs(): HasMany + { + return $this->hasMany(PhotoJob::class); + } + + public function scopeActive($query) + { + return $query->where('is_active', true)->orderBy('sort_order'); + } +} diff --git a/database/migrations/2026_05_30_122400_add_session_setting_to_photo_jobs_table.php b/database/migrations/2026_05_30_122400_add_session_setting_to_photo_jobs_table.php new file mode 100644 index 0000000..9eba7cf --- /dev/null +++ b/database/migrations/2026_05_30_122400_add_session_setting_to_photo_jobs_table.php @@ -0,0 +1,31 @@ +foreignId('photo_session_id')->nullable()->constrained('photo_sessions')->nullOnDelete(); + $table->foreignId('photo_setting_id')->nullable()->constrained('photo_settings')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('photo_jobs', function (Blueprint $table) { + $table->dropForeign(['photo_session_id']); + $table->dropForeign(['photo_setting_id']); + $table->dropColumn(['photo_session_id', 'photo_setting_id']); + }); + } +}; diff --git a/database/migrations/2026_05_30_122400_create_photo_sessions_table.php b/database/migrations/2026_05_30_122400_create_photo_sessions_table.php new file mode 100644 index 0000000..5d80993 --- /dev/null +++ b/database/migrations/2026_05_30_122400_create_photo_sessions_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('original_image_path'); + $table->unsignedBigInteger('selected_job_id')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('photo_sessions'); + } +}; diff --git a/database/migrations/2026_05_30_122400_create_photo_settings_table.php b/database/migrations/2026_05_30_122400_create_photo_settings_table.php new file mode 100644 index 0000000..e870838 --- /dev/null +++ b/database/migrations/2026_05_30_122400_create_photo_settings_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->text('gmic_command'); + $table->integer('sort_order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('photo_settings'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..59cb778 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -15,11 +15,6 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); - - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]); + $this->call(PhotoSettingSeeder::class); } } diff --git a/database/seeders/PhotoSettingSeeder.php b/database/seeders/PhotoSettingSeeder.php new file mode 100644 index 0000000..9730d43 --- /dev/null +++ b/database/seeders/PhotoSettingSeeder.php @@ -0,0 +1,37 @@ + 'Comic', + 'gmic_command' => 'cl_comic 4,1,0,0,1,15,15,1,10,20,6,2,0,0,0,0,0,0,50,50', + 'sort_order' => 1, + 'is_active' => true, + ], + [ + 'name' => 'Schwarzweiß', + 'gmic_command' => '-to_gray', + 'sort_order' => 2, + 'is_active' => true, + ], + [ + 'name' => 'Weichzeichner', + 'gmic_command' => '-blur 4 -sharpen 80', + 'sort_order' => 3, + 'is_active' => true, + ], + ]; + + foreach ($settings as $setting) { + PhotoSetting::firstOrCreate(['name' => $setting['name']], $setting); + } + } +} diff --git a/resources/views/camera.blade.php b/resources/views/camera.blade.php index bf05846..dcfe7ea 100644 --- a/resources/views/camera.blade.php +++ b/resources/views/camera.blade.php @@ -15,6 +15,8 @@ @livewire('camera-capture') + @livewire('photo-settings') + @livewireScripts diff --git a/resources/views/livewire/camera-capture.blade.php b/resources/views/livewire/camera-capture.blade.php index 00616c1..8442920 100644 --- a/resources/views/livewire/camera-capture.blade.php +++ b/resources/views/livewire/camera-capture.blade.php @@ -1,7 +1,8 @@
+ {{-- Camera preview --}}
@@ -19,46 +20,136 @@ wire:loading.attr="disabled" @disabled($hasProcessing) class="mt-8 px-10 py-4 bg-white text-gray-950 font-semibold text-lg rounded-full shadow-lg hover:bg-gray-200 active:scale-95 transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed"> - {{ $hasProcessing ? 'Erstelle Bild...' : 'Foto aufnehmen' }} - Saving… + {{ $hasProcessing ? 'Erstelle Varianten...' : 'Foto aufnehmen' }} + Speichern…
{{ $status }}
- @if ($recentPhotos->isNotEmpty()) -
-

Recent Photos

-
- @foreach ($recentPhotos as $photo) -
- @if ($photo['status'] === 'processing') -
- Photo #{{ $photo['id'] }} -
- - - - -
-
- @else - Photo #{{ $photo['id'] }} - - @endif - - {{ $photo['status'] }} - -
+ {{-- Tabbed photo sessions --}} + @if ($currentSession || $previousSessions->isNotEmpty()) +
+ + {{-- Tab navigation --}} +
+ + @foreach ($previousSessions as $session) + @endforeach
+ + {{-- Current session tab --}} +
+ @if ($currentSession) +
+ + {{-- Original for comparison --}} +
+ Original + + Original + +
+ + {{-- Variants --}} + @foreach ($currentSession->jobs as $job) +
status === 'done') x-on:click="$wire.selectVariant({{ $currentSession->id }}, {{ $job->id }})" @endif> + + @if ($job->status === 'processing') +
+ + + + +
+ @elseif ($job->status === 'failed') +
+ Fehler +
+ @else + {{ $job->setting?->name }} + @if ($currentSession->selected_job_id === $job->id) +
+ + + +
+ @endif + @endif + + + {{ $job->setting?->name ?? 'Filter' }} + +
+ @endforeach + +
+ + + @else +

Noch kein Foto aufgenommen.

+ @endif +
+ + {{-- Previous session tabs --}} + @foreach ($previousSessions as $session) +
+ @php + $displayJob = $session->selected_job_id + ? $session->jobs->firstWhere('id', $session->selected_job_id) + : $session->jobs->where('status', 'done')->first(); + @endphp + + @if ($displayJob) + Session #{{ $session->id }} + @endif + + {{-- Variant thumbnails --}} + @if ($session->jobs->count() > 1) +
+ @foreach ($session->jobs->where('status', 'done') as $job) +
+ {{ $job->setting?->name }} + + {{ $job->setting?->name ?? 'Filter' }} + +
+ @endforeach +
+ @endif + + +
+ @endforeach +
@endif @@ -69,8 +160,7 @@ class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-6" style="display:none">
- Photo + Photo
diff --git a/resources/views/livewire/photo-settings.blade.php b/resources/views/livewire/photo-settings.blade.php new file mode 100644 index 0000000..8a69d39 --- /dev/null +++ b/resources/views/livewire/photo-settings.blade.php @@ -0,0 +1,69 @@ +
+
+

Foto-Einstellungen

+ +
+ + @if ($showForm) +
+ + + +
+ @endif + +
+ @foreach ($settings as $setting) +
+ + + @if (isset($editingId[$setting->id])) +
+ + + + +
+ @else +
+

{{ $setting->name }}

+

{{ $setting->gmic_command }}

+
+
+ + +
+ @endif +
+ @endforeach + + @if ($settings->isEmpty()) +

Keine Einstellungen vorhanden.

+ @endif +
+