feat: add theme

This commit is contained in:
2026-06-14 15:06:11 +03:30
parent fb17f4c08b
commit b1e39dfabf
8 changed files with 258 additions and 7 deletions
+12 -6
View File
@@ -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'),
]);
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers;
use App\Models\Theme;
use Illuminate\Http\Request;
class ThemeController extends Controller
{
// List themes (for the scene theme picker and the app).
public function index(Request $request)
{
$query = Theme::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(Theme::findOrFail($id));
}
}
+7 -1
View File
@@ -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
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Theme extends Model
{
protected $fillable = ['key', 'name', 'colors', 'order', 'is_active'];
protected $casts = [
'colors' => 'array',
'is_active' => 'boolean',
'order' => 'integer',
];
public function scenes(): HasMany
{
return $this->hasMany(Scene::class);
}
}
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('themes', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('scenes', function (Blueprint $table) {
$table->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');
});
}
};
+136
View File
@@ -0,0 +1,136 @@
<?php
namespace Database\Seeders;
use App\Models\Theme;
use Illuminate\Database\Seeder;
class ThemeSeeder extends Seeder
{
public function run(): void
{
$common = [
'bgPrimary' => '#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);
}
}
}
+5
View File
@@ -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']);