feat: add theme
This commit is contained in:
@@ -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'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user