From b1e39dfabfe80d0926e1307fbe5238ac725b7e7f Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Sun, 14 Jun 2026 15:06:11 +0330 Subject: [PATCH] feat: add theme --- app/Http/Controllers/SceneController.php | 18 ++- app/Http/Controllers/ThemeController.php | 26 ++++ app/Models/Scene.php | 8 +- app/Models/Theme.php | 22 +++ .../2026_06_15_100000_create_themes_table.php | 26 ++++ ...15_100100_add_theme_id_to_scenes_table.php | 24 ++++ database/seeders/ThemeSeeder.php | 136 ++++++++++++++++++ routes/api.php | 5 + 8 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 app/Http/Controllers/ThemeController.php create mode 100644 app/Models/Theme.php create mode 100644 database/migrations/2026_06_15_100000_create_themes_table.php create mode 100644 database/migrations/2026_06_15_100100_add_theme_id_to_scenes_table.php create mode 100644 database/seeders/ThemeSeeder.php diff --git a/app/Http/Controllers/SceneController.php b/app/Http/Controllers/SceneController.php index be5c0b6..4b2d1d9 100644 --- a/app/Http/Controllers/SceneController.php +++ b/app/Http/Controllers/SceneController.php @@ -12,11 +12,11 @@ class SceneController extends Controller // CONSOLIDATED: everything the scene-settings screen needs in one call. public function settings() { - $settings = UserSceneSetting::with('activeScene') + $settings = UserSceneSetting::with('activeScene.theme') ->firstOrNew(['user_id' => auth()->id()]); return response()->json([ - 'scenes' => Scene::where('is_active', true)->orderBy('order')->get(), + 'scenes' => Scene::with('theme')->where('is_active', true)->orderBy('order')->get(), 'settings' => [ 'active_scene_id' => $settings->active_scene_id, 'active_scene' => $settings->activeScene, @@ -55,7 +55,7 @@ public function updateSettings(Request $request) public function index(Request $request) { - $query = Scene::query(); + $query = Scene::query()->with('theme'); if (!$request->boolean('include_inactive')) { $query->where('is_active', true); @@ -66,7 +66,7 @@ public function index(Request $request) public function show($id) { - return response()->json(Scene::findOrFail($id)); + return response()->json(Scene::with('theme')->findOrFail($id)); } // CREATE a scene with its image / video / sound uploads. @@ -74,6 +74,7 @@ public function store(Request $request) { $data = $request->validate([ 'name' => 'required|string|max:255', + 'theme_id' => 'nullable|exists:themes,id', 'order' => 'nullable|integer', 'is_active' => 'nullable|boolean', 'image' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192', @@ -83,6 +84,7 @@ public function store(Request $request) $scene = Scene::create([ 'name' => $data['name'], + 'theme_id' => $data['theme_id'] ?? null, 'order' => $data['order'] ?? 0, 'is_active' => $data['is_active'] ?? true, 'image_path' => $request->hasFile('image') ? $request->file('image')->store('scenes/images', 'public') : null, @@ -92,7 +94,7 @@ public function store(Request $request) return response()->json([ 'message' => 'Scene created successfully', - 'scene' => $scene, + 'scene' => $scene->load('theme'), ], 201); } @@ -103,6 +105,7 @@ public function update(Request $request, $id) $data = $request->validate([ 'name' => 'sometimes|string|max:255', + 'theme_id' => 'nullable|exists:themes,id', 'order' => 'nullable|integer', 'is_active' => 'nullable|boolean', 'image' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192', @@ -113,6 +116,9 @@ public function update(Request $request, $id) if (array_key_exists('name', $data)) { $scene->name = $data['name']; } + if (array_key_exists('theme_id', $data)) { + $scene->theme_id = $data['theme_id']; + } if (array_key_exists('order', $data)) { $scene->order = $data['order']; } @@ -134,7 +140,7 @@ public function update(Request $request, $id) return response()->json([ 'message' => 'Scene updated successfully', - 'scene' => $scene, + 'scene' => $scene->load('theme'), ]); } diff --git a/app/Http/Controllers/ThemeController.php b/app/Http/Controllers/ThemeController.php new file mode 100644 index 0000000..a6c5b5d --- /dev/null +++ b/app/Http/Controllers/ThemeController.php @@ -0,0 +1,26 @@ +boolean('include_inactive')) { + $query->where('is_active', true); + } + + return response()->json($query->orderBy('order')->get()); + } + + public function show($id) + { + return response()->json(Theme::findOrFail($id)); + } +} diff --git a/app/Models/Scene.php b/app/Models/Scene.php index 5ded102..e656c10 100644 --- a/app/Models/Scene.php +++ b/app/Models/Scene.php @@ -3,16 +3,22 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class Scene extends Model { - protected $fillable = ['name', 'image_path', 'video_path', 'sound_path', 'order', 'is_active']; + protected $fillable = ['theme_id', 'name', 'image_path', 'video_path', 'sound_path', 'order', 'is_active']; protected $casts = [ 'is_active' => 'boolean', 'order' => 'integer', ]; + public function theme(): BelongsTo + { + return $this->belongsTo(Theme::class); + } + protected $appends = ['image_url', 'video_url', 'sound_url', 'has_video']; public function getImageUrlAttribute(): ?string diff --git a/app/Models/Theme.php b/app/Models/Theme.php new file mode 100644 index 0000000..f37219c --- /dev/null +++ b/app/Models/Theme.php @@ -0,0 +1,22 @@ + 'array', + 'is_active' => 'boolean', + 'order' => 'integer', + ]; + + public function scenes(): HasMany + { + return $this->hasMany(Scene::class); + } +} diff --git a/database/migrations/2026_06_15_100000_create_themes_table.php b/database/migrations/2026_06_15_100000_create_themes_table.php new file mode 100644 index 0000000..280eca2 --- /dev/null +++ b/database/migrations/2026_06_15_100000_create_themes_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('key')->unique(); // blue, pink, green, orange, purple + $table->string('name'); // display label + $table->json('colors'); // full color map (json works on MySQL & Postgres) + $table->integer('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('themes'); + } +}; diff --git a/database/migrations/2026_06_15_100100_add_theme_id_to_scenes_table.php b/database/migrations/2026_06_15_100100_add_theme_id_to_scenes_table.php new file mode 100644 index 0000000..6eb506a --- /dev/null +++ b/database/migrations/2026_06_15_100100_add_theme_id_to_scenes_table.php @@ -0,0 +1,24 @@ +foreignId('theme_id')->nullable()->after('id') + ->constrained('themes')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('scenes', function (Blueprint $table) { + $table->dropForeign(['theme_id']); + $table->dropColumn('theme_id'); + }); + } +}; diff --git a/database/seeders/ThemeSeeder.php b/database/seeders/ThemeSeeder.php new file mode 100644 index 0000000..7241426 --- /dev/null +++ b/database/seeders/ThemeSeeder.php @@ -0,0 +1,136 @@ + '#26000000', + 'bgSecondary' => '#33000000', + 'bgTertiary' => '#40000000', + 'bgFourth' => '#80000000', + 'bgBlack' => '#99000000', + 'bgWhite' => '#FFF7F7F7', + 'bgError' => '#FFF04438', + 'fgPrimary' => '#FFFFFFFF', + 'fgSecondary' => '#FFD9D9D9', + 'fgDisable' => '#FFC0C0C0', + 'fgBlack' => '#FF000000', + 'fgBlack2' => '#FF2F2F2F', + 'fgError' => '#FFF04438', + 'textPrimary' => '#FFFFFFFF', + 'textSecondary' => '#FFD9D9D9', + 'textDisable' => '#FFC0C0C0', + 'textPrimaryReverse' => '#FF000000', + 'textPrimaryReverse2' => '#FF2F2F2F', + 'textError' => '#FFF04438', + 'borderPrimary' => '#26FFFFFF', + 'borderSecondary' => '#40FFFFFF', + 'borderWhite' => '#FFFFFFFF', + 'borderBlack' => '#FF000000', + 'borderError' => '#FFF04438', + ]; + + $themes = [ + [ + 'key' => 'blue', + 'name' => 'آبی', + 'order' => 1, + 'colors' => array_merge($common, [ + 'themeUp' => '#156395', + 'themeDown' => '#2E3381', + 'bottomNavigation' => '#292E74', + 'lightColorGradient' => '#3ABDEB', + 'darkColorGradient' => '#553AEB', + 'chatContainer' => '#252967', + 'bottomPlayerContainer' => '#252967', + 'bgBrand' => '#5360FC', + 'fgBrand' => '#5360FC', + 'textBrand' => '#5360FC', + 'borderBrand' => '#5360FC', + ]), + ], + [ + 'key' => 'pink', + 'name' => 'صورتی', + 'order' => 2, + 'colors' => array_merge($common, [ + 'themeUp' => '#843F6A', + 'themeDown' => '#4F366F', + 'bottomNavigation' => '#3F2B59', + 'lightColorGradient' => '#CE4299', + 'darkColorGradient' => '#8628FF', + 'chatContainer' => '#3F2B59', + 'bottomPlayerContainer' => '#3F2B59', + 'bgBrand' => '#843F6A', + 'fgBrand' => '#843F6A', + 'textBrand' => '#FFFF80AB', + 'borderBrand' => '#843F6A', + ]), + ], + [ + 'key' => 'green', + 'name' => 'سبز', + 'order' => 3, + 'colors' => array_merge($common, [ + 'themeUp' => '#3B6D3B', + 'themeDown' => '#064A3F', + 'bottomNavigation' => '#054339', + 'lightColorGradient' => '#5CC65C', + 'darkColorGradient' => '#00A489', + 'chatContainer' => '#053B32', + 'bottomPlayerContainer' => '#053B32', + 'bgBrand' => '#3B6D3B', + 'fgBrand' => '#3B6D3B', + 'textBrand' => '#4CAF50', + 'borderBrand' => '#3B6D3B', + ]), + ], + [ + 'key' => 'orange', + 'name' => 'نارنجی', + 'order' => 4, + 'colors' => array_merge($common, [ + 'themeUp' => '#7E5607', + 'themeDown' => '#81381D', + 'bottomNavigation' => '#74321A', + 'lightColorGradient' => '#E2990A', + 'darkColorGradient' => '#CC4B1C', + 'chatContainer' => '#672D17', + 'bottomPlayerContainer' => '#672D17', + 'bgBrand' => '#7E5607', + 'fgBrand' => '#7E5607', + 'textBrand' => '#FFE2990A', + 'borderBrand' => '#7E5607', + ]), + ], + [ + 'key' => 'purple', + 'name' => 'بنفش', + 'order' => 5, + 'colors' => array_merge($common, [ + 'themeUp' => '#371F63', + 'themeDown' => '#132F6F', + 'bottomNavigation' => '#112A64', + 'lightColorGradient' => '#6829DD', + 'darkColorGradient' => '#1A50CC', + 'chatContainer' => '#0F2659', + 'bottomPlayerContainer' => '#0F2659', + 'bgBrand' => '#371F63', + 'fgBrand' => '#371F63', + 'textBrand' => '#1A50CC', + 'borderBrand' => '#371F63', + ]), + ], + ]; + + foreach ($themes as $theme) { + Theme::updateOrCreate(['key' => $theme['key']], $theme); + } + } +} diff --git a/routes/api.php b/routes/api.php index 2ca7305..318b858 100644 --- a/routes/api.php +++ b/routes/api.php @@ -16,6 +16,7 @@ use App\Http\Controllers\AppFeedbackController; use App\Http\Controllers\SliderController; use App\Http\Controllers\SceneController; +use App\Http\Controllers\ThemeController; use App\Http\Controllers\BellSoundController; use App\Http\Controllers\BackgroundSoundController; use App\Http\Controllers\TimerPresetController; @@ -182,6 +183,10 @@ Route::delete('/slider/{id}', [SliderController::class, 'destroy']); // delete slider + /// themes (color themes a scene can use) + Route::get('/themes', [ThemeController::class, 'index']); + Route::get('/themes/{id}', [ThemeController::class, 'show']); + /// 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']);