feat: add scene settings feature
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Scene;
|
||||
use App\Models\UserSceneSetting;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class SceneController extends Controller
|
||||
{
|
||||
// CONSOLIDATED: everything the scene-settings screen needs in one call.
|
||||
public function settings()
|
||||
{
|
||||
$settings = UserSceneSetting::with('activeScene')
|
||||
->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']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Scene extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'image_path', 'video_path', 'sound_path', 'order', 'is_active'];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => '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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserSceneSetting extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'active_scene_id',
|
||||
'scene_volume',
|
||||
'background_play_seconds',
|
||||
'video_enabled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'scene_volume' => '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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('scenes', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_scene_settings', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user