From 82164f691fce84ab55db8b0150fc14109b7e1b7d Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Tue, 2 Jun 2026 23:31:40 +0330 Subject: [PATCH] feat: add scene settings feature --- app/Http/Controllers/SceneController.php | 155 ++++++++++++++++++ app/Models/Scene.php | 38 +++++ app/Models/UserSceneSetting.php | 33 ++++ .../2026_06_01_130000_create_scenes_table.php | 33 ++++ ...40000_create_user_scene_settings_table.php | 34 ++++ routes/api.php | 12 ++ 6 files changed, 305 insertions(+) create mode 100644 app/Http/Controllers/SceneController.php create mode 100644 app/Models/Scene.php create mode 100644 app/Models/UserSceneSetting.php create mode 100644 database/migrations/2026_06_01_130000_create_scenes_table.php create mode 100644 database/migrations/2026_06_01_140000_create_user_scene_settings_table.php diff --git a/app/Http/Controllers/SceneController.php b/app/Http/Controllers/SceneController.php new file mode 100644 index 0000000..1b83686 --- /dev/null +++ b/app/Http/Controllers/SceneController.php @@ -0,0 +1,155 @@ +firstOrNew(['user_id' => auth()->id()]); + + return response()->json([ + 'scenes' => Scene::where('is_active', true)->orderBy('order')->get(), + 'settings' => [ + 'active_scene_id' => $settings->active_scene_id, + 'active_scene' => $settings->activeScene, + 'scene_volume' => $settings->scene_volume ?? 100, + 'background_play_seconds' => $settings->background_play_seconds ?? 0, + 'video_enabled' => (bool) ($settings->video_enabled ?? false), + ], + ]); + } + + // Persist the current user's scene preferences. + public function updateSettings(Request $request) + { + $data = $request->validate([ + 'active_scene_id' => 'nullable|exists:scenes,id', + 'scene_volume' => 'nullable|integer|min:0|max:100', + 'background_play_seconds' => 'nullable|integer|min:0', + 'video_enabled' => 'nullable|boolean', + ]); + + $settings = UserSceneSetting::firstOrNew(['user_id' => auth()->id()]); + + foreach (['active_scene_id', 'scene_volume', 'background_play_seconds', 'video_enabled'] as $field) { + if ($request->has($field)) { + $settings->$field = $data[$field] ?? ($field === 'video_enabled' ? false : null); + } + } + + $settings->save(); + + return response()->json([ + 'message' => 'Settings saved successfully', + 'settings' => $settings->load('activeScene'), + ]); + } + + public function index(Request $request) + { + $query = Scene::query(); + + if (!$request->boolean('include_inactive')) { + $query->where('is_active', true); + } + + return response()->json($query->orderBy('order')->get()); + } + + public function show($id) + { + return response()->json(Scene::findOrFail($id)); + } + + // CREATE a scene with its image / video / sound uploads. + public function store(Request $request) + { + $data = $request->validate([ + 'name' => 'required|string|max:255', + 'order' => 'nullable|integer', + 'is_active' => 'nullable|boolean', + 'image' => 'nullable|image|max:8192', + 'video' => 'nullable|mimes:mp4,mov,webm|max:512000', + 'sound' => 'nullable|mimes:mp3,wav,ogg,flac|max:51200', + ]); + + $scene = Scene::create([ + 'name' => $data['name'], + 'order' => $data['order'] ?? 0, + 'is_active' => $data['is_active'] ?? true, + 'image_path' => $request->hasFile('image') ? $request->file('image')->store('scenes/images', 'public') : null, + 'video_path' => $request->hasFile('video') ? $request->file('video')->store('scenes/videos', 'public') : null, + 'sound_path' => $request->hasFile('sound') ? $request->file('sound')->store('scenes/sounds', 'public') : null, + ]); + + return response()->json([ + 'message' => 'Scene created successfully', + 'scene' => $scene, + ], 201); + } + + // UPDATE a scene; any uploaded file replaces the old one (POST for multipart support). + public function update(Request $request, $id) + { + $scene = Scene::findOrFail($id); + + $data = $request->validate([ + 'name' => 'sometimes|string|max:255', + 'order' => 'nullable|integer', + 'is_active' => 'nullable|boolean', + 'image' => 'nullable|image|max:8192', + 'video' => 'nullable|mimes:mp4,mov,webm|max:512000', + 'sound' => 'nullable|mimes:mp3,wav,ogg,flac|max:51200', + ]); + + if (array_key_exists('name', $data)) { + $scene->name = $data['name']; + } + if (array_key_exists('order', $data)) { + $scene->order = $data['order']; + } + if (array_key_exists('is_active', $data)) { + $scene->is_active = $data['is_active']; + } + + foreach (['image' => 'scenes/images', 'video' => 'scenes/videos', 'sound' => 'scenes/sounds'] as $field => $folder) { + if ($request->hasFile($field)) { + $column = $field === 'image' ? 'image_path' : ($field === 'video' ? 'video_path' : 'sound_path'); + if ($scene->$column) { + Storage::disk('public')->delete($scene->$column); + } + $scene->$column = $request->file($field)->store($folder, 'public'); + } + } + + $scene->save(); + + return response()->json([ + 'message' => 'Scene updated successfully', + 'scene' => $scene, + ]); + } + + public function destroy($id) + { + $scene = Scene::findOrFail($id); + + foreach (['image_path', 'video_path', 'sound_path'] as $column) { + if ($scene->$column) { + Storage::disk('public')->delete($scene->$column); + } + } + + $scene->delete(); + + return response()->json(['message' => 'Scene deleted successfully']); + } +} diff --git a/app/Models/Scene.php b/app/Models/Scene.php new file mode 100644 index 0000000..5ded102 --- /dev/null +++ b/app/Models/Scene.php @@ -0,0 +1,38 @@ + 'boolean', + 'order' => 'integer', + ]; + + protected $appends = ['image_url', 'video_url', 'sound_url', 'has_video']; + + public function getImageUrlAttribute(): ?string + { + return $this->image_path ? asset('storage/' . $this->image_path) : null; + } + + public function getVideoUrlAttribute(): ?string + { + return $this->video_path ? asset('storage/' . $this->video_path) : null; + } + + public function getSoundUrlAttribute(): ?string + { + return $this->sound_path ? asset('storage/' . $this->sound_path) : null; + } + + // Whether this scene can be shown as a video (the "تبدیل صحنه به ویدیو" toggle). + public function getHasVideoAttribute(): bool + { + return !empty($this->video_path); + } +} diff --git a/app/Models/UserSceneSetting.php b/app/Models/UserSceneSetting.php new file mode 100644 index 0000000..03faa2c --- /dev/null +++ b/app/Models/UserSceneSetting.php @@ -0,0 +1,33 @@ + 'integer', + 'background_play_seconds' => 'integer', + 'video_enabled' => 'boolean', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function activeScene(): BelongsTo + { + return $this->belongsTo(Scene::class, 'active_scene_id'); + } +} diff --git a/database/migrations/2026_06_01_130000_create_scenes_table.php b/database/migrations/2026_06_01_130000_create_scenes_table.php new file mode 100644 index 0000000..bf9498c --- /dev/null +++ b/database/migrations/2026_06_01_130000_create_scenes_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name'); + $table->string('image_path')->nullable(); // scene background image + $table->string('video_path')->nullable(); // animated/video version of the scene + $table->string('sound_path')->nullable(); // scene ambient sound + $table->integer('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('scenes'); + } +}; diff --git a/database/migrations/2026_06_01_140000_create_user_scene_settings_table.php b/database/migrations/2026_06_01_140000_create_user_scene_settings_table.php new file mode 100644 index 0000000..4c92e28 --- /dev/null +++ b/database/migrations/2026_06_01_140000_create_user_scene_settings_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('active_scene_id')->nullable()->constrained('scenes')->nullOnDelete(); + $table->unsignedInteger('scene_volume')->default(100); // صدای صحنه (0-100) + $table->unsignedInteger('background_play_seconds')->default(0); // پخش صدا خارج از برنامه + $table->boolean('video_enabled')->default(false); // تبدیل صحنه به ویدیو + $table->timestamps(); + + $table->unique('user_id'); // one settings row per user + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_scene_settings'); + } +}; diff --git a/routes/api.php b/routes/api.php index ba86ae2..f43e0d8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,6 +13,7 @@ use App\Http\Controllers\QuestionController; use App\Http\Controllers\SurveyQuestionController; use App\Http\Controllers\SliderController; +use App\Http\Controllers\SceneController; use App\Http\Controllers\ImageController; use App\Http\Controllers\MusicController; use App\Http\Controllers\MediaController; @@ -158,6 +159,17 @@ Route::delete('/slider/{id}', [SliderController::class, 'destroy']); // delete slider + /// scenes (تنظیمات صحنه) — each scene has an image, optional video, and sound + // Consolidated settings screen (all scenes + current user's preferences) in one call. + Route::get('/scene-settings', [SceneController::class, 'settings']); + Route::put('/scene-settings', [SceneController::class, 'updateSettings']); + Route::get('/scenes', [SceneController::class, 'index']); + Route::post('/scenes', [SceneController::class, 'store']); // multipart: image, video, sound + Route::get('/scenes/{id}', [SceneController::class, 'show']); + Route::post('/scenes/{id}', [SceneController::class, 'update']); // multipart update + Route::delete('/scenes/{id}', [SceneController::class, 'destroy']); + + ///media Route::get('/media/filters', [MediaController::class, 'filters']);