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.
|
// CONSOLIDATED: everything the scene-settings screen needs in one call.
|
||||||
public function settings()
|
public function settings()
|
||||||
{
|
{
|
||||||
$settings = UserSceneSetting::with('activeScene')
|
$settings = UserSceneSetting::with('activeScene.theme')
|
||||||
->firstOrNew(['user_id' => auth()->id()]);
|
->firstOrNew(['user_id' => auth()->id()]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'scenes' => Scene::where('is_active', true)->orderBy('order')->get(),
|
'scenes' => Scene::with('theme')->where('is_active', true)->orderBy('order')->get(),
|
||||||
'settings' => [
|
'settings' => [
|
||||||
'active_scene_id' => $settings->active_scene_id,
|
'active_scene_id' => $settings->active_scene_id,
|
||||||
'active_scene' => $settings->activeScene,
|
'active_scene' => $settings->activeScene,
|
||||||
@@ -55,7 +55,7 @@ public function updateSettings(Request $request)
|
|||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$query = Scene::query();
|
$query = Scene::query()->with('theme');
|
||||||
|
|
||||||
if (!$request->boolean('include_inactive')) {
|
if (!$request->boolean('include_inactive')) {
|
||||||
$query->where('is_active', true);
|
$query->where('is_active', true);
|
||||||
@@ -66,7 +66,7 @@ public function index(Request $request)
|
|||||||
|
|
||||||
public function show($id)
|
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.
|
// CREATE a scene with its image / video / sound uploads.
|
||||||
@@ -74,6 +74,7 @@ public function store(Request $request)
|
|||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
|
'theme_id' => 'nullable|exists:themes,id',
|
||||||
'order' => 'nullable|integer',
|
'order' => 'nullable|integer',
|
||||||
'is_active' => 'nullable|boolean',
|
'is_active' => 'nullable|boolean',
|
||||||
'image' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192',
|
'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([
|
$scene = Scene::create([
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
|
'theme_id' => $data['theme_id'] ?? null,
|
||||||
'order' => $data['order'] ?? 0,
|
'order' => $data['order'] ?? 0,
|
||||||
'is_active' => $data['is_active'] ?? true,
|
'is_active' => $data['is_active'] ?? true,
|
||||||
'image_path' => $request->hasFile('image') ? $request->file('image')->store('scenes/images', 'public') : null,
|
'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([
|
return response()->json([
|
||||||
'message' => 'Scene created successfully',
|
'message' => 'Scene created successfully',
|
||||||
'scene' => $scene,
|
'scene' => $scene->load('theme'),
|
||||||
], 201);
|
], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +105,7 @@ public function update(Request $request, $id)
|
|||||||
|
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'name' => 'sometimes|string|max:255',
|
'name' => 'sometimes|string|max:255',
|
||||||
|
'theme_id' => 'nullable|exists:themes,id',
|
||||||
'order' => 'nullable|integer',
|
'order' => 'nullable|integer',
|
||||||
'is_active' => 'nullable|boolean',
|
'is_active' => 'nullable|boolean',
|
||||||
'image' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192',
|
'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)) {
|
if (array_key_exists('name', $data)) {
|
||||||
$scene->name = $data['name'];
|
$scene->name = $data['name'];
|
||||||
}
|
}
|
||||||
|
if (array_key_exists('theme_id', $data)) {
|
||||||
|
$scene->theme_id = $data['theme_id'];
|
||||||
|
}
|
||||||
if (array_key_exists('order', $data)) {
|
if (array_key_exists('order', $data)) {
|
||||||
$scene->order = $data['order'];
|
$scene->order = $data['order'];
|
||||||
}
|
}
|
||||||
@@ -134,7 +140,7 @@ public function update(Request $request, $id)
|
|||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Scene updated successfully',
|
'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;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class Scene extends Model
|
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 = [
|
protected $casts = [
|
||||||
'is_active' => 'boolean',
|
'is_active' => 'boolean',
|
||||||
'order' => 'integer',
|
'order' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function theme(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Theme::class);
|
||||||
|
}
|
||||||
|
|
||||||
protected $appends = ['image_url', 'video_url', 'sound_url', 'has_video'];
|
protected $appends = ['image_url', 'video_url', 'sound_url', 'has_video'];
|
||||||
|
|
||||||
public function getImageUrlAttribute(): ?string
|
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\AppFeedbackController;
|
||||||
use App\Http\Controllers\SliderController;
|
use App\Http\Controllers\SliderController;
|
||||||
use App\Http\Controllers\SceneController;
|
use App\Http\Controllers\SceneController;
|
||||||
|
use App\Http\Controllers\ThemeController;
|
||||||
use App\Http\Controllers\BellSoundController;
|
use App\Http\Controllers\BellSoundController;
|
||||||
use App\Http\Controllers\BackgroundSoundController;
|
use App\Http\Controllers\BackgroundSoundController;
|
||||||
use App\Http\Controllers\TimerPresetController;
|
use App\Http\Controllers\TimerPresetController;
|
||||||
@@ -182,6 +183,10 @@
|
|||||||
Route::delete('/slider/{id}', [SliderController::class, 'destroy']); // delete slider
|
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
|
/// scenes (تنظیمات صحنه) — each scene has an image, optional video, and sound
|
||||||
// Consolidated settings screen (all scenes + current user's preferences) in one call.
|
// Consolidated settings screen (all scenes + current user's preferences) in one call.
|
||||||
Route::get('/scene-settings', [SceneController::class, 'settings']);
|
Route::get('/scene-settings', [SceneController::class, 'settings']);
|
||||||
|
|||||||
Reference in New Issue
Block a user