From a2709139314d9c2270f580a1d35d073813e6f30c Mon Sep 17 00:00:00 2001 From: josh Date: Mon, 15 Jun 2026 12:37:14 +1000 Subject: [PATCH] Added User Settings --- app/Http/Controllers/FlightController.php | 2 +- .../Controllers/FlightProfileController.php | 2 +- app/Http/Controllers/SettingsController.php | 29 ++++ app/Http/Controllers/UserController.php | 15 ++ app/Models/Airline.php | 8 +- app/Models/User.php | 42 ++++-- app/Models/UserFlight.php | 6 +- app/Settings/SettingsRegistry.php | 131 ++++++++++++++++ .../2026_06_14_060343_add_user_settings.php | 6 +- .../FlightsGoneBy/AircraftToolTip.vue | 7 + .../FlightsGoneBy/DepartureBoard.vue | 33 +++-- .../FlightsGoneBy/DepartureBoardTableRow.vue | 29 ++-- .../FlightsGoneBy/FlightStatsBar.vue | 4 +- .../Components/FlightsGoneBy/MainHeader.vue | 2 + resources/js/Pages/Profile/Edit.vue | 56 ------- resources/js/Pages/UserAchievements.vue | 2 +- resources/js/Pages/UserSettings.vue | 140 ++++++++++++++++++ resources/js/Types/types.d.ts | 28 +++- routes/api.php | 7 + routes/web.php | 4 + 20 files changed, 451 insertions(+), 102 deletions(-) create mode 100644 app/Http/Controllers/SettingsController.php create mode 100644 app/Settings/SettingsRegistry.php delete mode 100644 resources/js/Pages/Profile/Edit.vue create mode 100644 resources/js/Pages/UserSettings.vue diff --git a/app/Http/Controllers/FlightController.php b/app/Http/Controllers/FlightController.php index 14dccfd..1d49de9 100644 --- a/app/Http/Controllers/FlightController.php +++ b/app/Http/Controllers/FlightController.php @@ -272,7 +272,7 @@ class FlightController extends Controller $updated = $flight->snapshot($flight->id); $this->recordChanges($flight, $dirty, $original, $updated); - return redirect()->route('profile.departure-board', [Auth::user()->name, $flight->id]); + return redirect()->route('profile.departure-board', [$flight->user->name, $flight->id]); } public function delete(UserFlight $flight, ?string $referrer = 'departure-board') diff --git a/app/Http/Controllers/FlightProfileController.php b/app/Http/Controllers/FlightProfileController.php index fee5f72..c14f9ee 100644 --- a/app/Http/Controllers/FlightProfileController.php +++ b/app/Http/Controllers/FlightProfileController.php @@ -17,7 +17,7 @@ class FlightProfileController extends Controller public function profileData(User $user, string $view, ?int $selectedFlightId = null) : array { return [ 'user' => $user, - 'canEdit' => (auth()->check() && auth()->id() === $user->id) || auth()->user()->hasRole('admin'), + 'canEdit' => (auth()->check() && auth()->id() === $user->id) || (auth()->check() && auth()->user()->hasRole('admin')), 'initialView' => $view, 'selectedFlightId' => $selectedFlightId, 'flight_api_url' => self::getUserFlightApiURL($user), diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php new file mode 100644 index 0000000..c5d8540 --- /dev/null +++ b/app/Http/Controllers/SettingsController.php @@ -0,0 +1,29 @@ +user(); + $current = array_merge(SettingsRegistry::defaults(), $user->settings ?? []); + $schema = SettingsRegistry::schema(); + + $fields = array_map(fn($field) => array_merge($field, [ + 'value' => $current[$field['key']] ?? $field['default'], + ]), $schema); + + return response()->json(['fields' => $fields]); + } + + public function update(Request $request) + { + $validated = $request->validate(SettingsRegistry::validationRules()); + $request->user()->updateSettings($validated['settings']); + return response()->json(['message' => 'Settings saved.']); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index eaab037..80bd64b 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -5,9 +5,11 @@ namespace App\Http\Controllers; use App\Models\Followee; use App\Models\Notification; use App\Models\User; +use App\Settings\SettingsRegistry; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Inertia\Inertia; class UserController extends Controller { @@ -37,4 +39,17 @@ class UserController extends Controller return response()->json(['following' => true]); } + + public function settings(){ + $user = auth()->user(); + $current = array_merge(SettingsRegistry::defaults(), $user->settings ?? []); + $fields = array_map(fn($field) => array_merge($field, [ + 'value' => $current[$field['key']] ?? $field['default'], + ]), SettingsRegistry::schema()); + + return Inertia::render('UserSettings', [ + 'fields' => $fields, + 'categories' => SettingsRegistry::categories(), + ]); + } } diff --git a/app/Models/Airline.php b/app/Models/Airline.php index f646195..d85aa7f 100644 --- a/app/Models/Airline.php +++ b/app/Models/Airline.php @@ -45,7 +45,13 @@ class Airline extends Model protected function logoUrl() : Attribute{ return Attribute::make( get: function () { - return config('app.logo_api_url') . "/airline/$this->internal_name/logo/tail"; + $user = auth()->user(); + $apiUrl = config('app.logo_api_url'); + if ($user && !$user->getSetting('ai_tail_logos')) { + return $apiUrl .'/airline/blank/logo/tail'; + } + + return $apiUrl . "/airline/$this->internal_name/logo/tail"; } ); } diff --git a/app/Models/User.php b/app/Models/User.php index fe7e754..788638a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,9 +3,11 @@ namespace App\Models; use App\Http\Controllers\UserFlightController; +use App\Settings\SettingsRegistry; use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -13,7 +15,7 @@ use App\Traits\HasAchievements; use Laravel\Sanctum\HasApiTokens; use Spatie\Permission\Traits\HasRoles; -#[Fillable(['name', 'email', 'password', 'distance_unit'])] +#[Fillable(['name', 'email', 'password', 'distance_unit', 'settings'])] #[Hidden(['password', 'remember_token'])] class User extends Authenticatable { @@ -21,24 +23,38 @@ class User extends Authenticatable /** @use HasFactory */ use HasFactory, HasAchievements, HasApiTokens, HasRoles; - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } + protected $casts = [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'settings' => 'array', + ]; + + protected $appends = ['resolved_settings']; public function achievements(): HasMany { return $this->hasMany(UserAchievement::class); } + public function getSetting(string $key): mixed + { + $defaults = SettingsRegistry::defaults(); + return $this->settings[$key] ?? $defaults[$key] ?? null; + } + + public function updateSettings(array $values): void + { + $current = array_merge(SettingsRegistry::defaults(), $this->settings ?? []); + $this->update(['settings' => array_merge($current, $values)]); + } + + protected function resolvedSettings(): Attribute + { + return Attribute::make( + get: fn() => array_merge(SettingsRegistry::defaults(), $this->settings ?? []) + ); + } + public function unlockedAchievements(): HasMany { return $this->achievements() diff --git a/app/Models/UserFlight.php b/app/Models/UserFlight.php index 72dfe68..b4cdb4d 100644 --- a/app/Models/UserFlight.php +++ b/app/Models/UserFlight.php @@ -227,6 +227,10 @@ class UserFlight extends Model return Attribute::make( get: function () { + $user = auth()?->user(); + + $useAi = !$user || $user->getSetting('ai_liveries'); + $apiUrl = config('app.logo_api_url'); if (!$this->aircraft) { @@ -234,7 +238,7 @@ class UserFlight extends Model } - if ($this->airline){ + if ($this->airline && $useAi){ $path = "images/liveries/{$this->airline->internal_name}_{$this->aircraft->designator}.png"; if (Storage::disk('local')->exists($path)) { $finalPath = $apiUrl."/airline/{$this->airline->internal_name}/livery/{$this->aircraft->designator}"; diff --git a/app/Settings/SettingsRegistry.php b/app/Settings/SettingsRegistry.php new file mode 100644 index 0000000..9794d8c --- /dev/null +++ b/app/Settings/SettingsRegistry.php @@ -0,0 +1,131 @@ + 'Select either metric or incorrect units.', + 'FlightsGoneBy Settings' => 'Settings for Site Behaviour', + 'AI Generated Content' => 'Airline tail logos and liveries are AI generated with human cleanup. If you would rather not see any AI, then our blank aircraft templates are human created.', + 'Account & Privacy' => 'Everything to do with your account.', + ]; + } + + public static function schema(): array + { + return [ + [ + 'key' => 'distance_unit', + 'type' => 'select', + 'label' => 'Distance Units', + 'category' => 'Units of Measurement', + 'default' => 'km', + 'options' => [ + ['value' => 'km', 'label' => 'Kilometres (km)'], + ['value' => 'mi', 'label' => 'Miles (mi)'], + ['value' => 'nm', 'label' => 'Nautical miles (nm)'], + ], + ], + [ + 'category' => 'Account & Privacy', + 'key' => 'private_profile', + 'type' => 'select', + 'label' => 'Account Privacy', + 'default' => 'public', + 'options' => [ + ['value' => 'public', 'label' => 'Public Profile Viewable By Everyone'], + ['value' => 'private', 'label' => 'Private Profile Viewable Only By You and Your Approved Followers'], + ] + ], + [ + 'key' => 'default_login_page', + 'type' => 'select', + 'label' => 'Default Page After Login', + 'category' => 'FlightsGoneBy Settings', + 'default' => 'feed_first', + 'options' => [ + ['value' => 'feed_first', 'label' => 'Feed if Following People, Profile if Not'], + ['value' => 'profile', 'label' => 'Your Profile'], + ['value' => 'feed', 'label' => 'Your Feed'], + ['value' => 'dashboard', 'label' => 'Your Dashboard'], + ], + ], + [ + 'key' => 'default_profile_view', + 'type' => 'select', + 'label' => 'Default View When Loading a Profile', + 'category' => 'FlightsGoneBy Settings', + 'default' => 'departure-board', + 'options' => [ + ['value' => 'departure-board', 'label' => 'Departure Board'], + ['value' => 'map', 'label' => 'Map'], + ['value' => 'boarding-passes', 'label' => 'Boarding Passes'], + ['value' => 'achievements', 'label' => 'Achievements'], + ], + ], + [ + 'category' => 'AI Generated Content', + 'key' => 'ai_liveries', + 'type' => 'checkbox', + 'label' => 'Show AI Generated Livery Images', + 'default' => true, + ], + [ + 'category' => 'AI Generated Content', + 'key' => 'ai_tail_logos', + 'type' => 'checkbox', + 'label' => 'Show AI Generated Tail Logos', + 'default' => true, + ], + [ + 'key' => 'departure_board_columns', + 'category' => 'FlightsGoneBy Settings', + 'type' => 'multiselect', + 'label' => 'Which columns to show on the Departure Board', + 'default' => ['airline', 'flight_number', 'from', 'to', 'departure_date', 'departure_time', 'arrival_time', 'duration', 'distance', 'aircraft', 'registration', 'class_seat_combined'], + 'options' => [ + ['value' => 'airline', 'label' => 'Airline'], + ['value' => 'flight_number', 'label' => 'Flight Number'], + ['value' => 'from', 'label' => 'From'], + ['value' => 'to', 'label' => 'To'], + ['value' => 'departure_date', 'label' => 'Departure Date'], + ['value' => 'departure_time', 'label' => 'Departure Time'], + ['value' => 'arrival_time', 'label' => 'Arrival Time'], + ['value' => 'duration', 'label' => 'Duration'], + ['value' => 'distance', 'label' => 'Distance'], + ['value' => 'aircraft', 'label' => 'Aircraft'], + ['value' => 'registration', 'label' => 'Aircraft Registration'], + ['value' => 'class_seat_combined', 'label' => 'Class/Seat Type/Seat Number Combined'], + ], + ], + ]; + } + + public static function defaults(): array + { + return collect(static::schema()) + ->pluck('default', 'key') + ->toArray(); + } + + public static function validationRules(): array + { + $rules = []; + foreach (static::schema() as $field) { + $key = "settings.{$field['key']}"; + $rules[$key] = match ($field['type']) { + 'select' => ['required', 'string', 'in:' . implode(',', array_column($field['options'], 'value'))], + 'checkbox' => ['boolean'], + 'text' => ['nullable', 'string', 'max:255'], + 'multiselect' => ['nullable', 'array'], + "settings.{$field['key']}.*" => ['string'], + default => ['nullable'], + }; + } + return $rules; + } +} diff --git a/database/migrations/2026_06_14_060343_add_user_settings.php b/database/migrations/2026_06_14_060343_add_user_settings.php index 88fa2f3..7b61ecd 100644 --- a/database/migrations/2026_06_14_060343_add_user_settings.php +++ b/database/migrations/2026_06_14_060343_add_user_settings.php @@ -9,11 +9,13 @@ return new class extends Migration /** * Run the migrations. */ +// database/migrations/xxxx_add_settings_to_users_table.php public function up(): void { - // + Schema::table('users', function (Blueprint $table) { + $table->json('settings')->nullable()->after('password'); + }); } - /** * Reverse the migrations. */ diff --git a/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue b/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue index 47a6a79..951193b 100644 --- a/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue +++ b/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue @@ -53,6 +53,13 @@ function formatEngineType(type: string): string {
+
+
+
+ Registration + {{ flight.aircraft_registration}} +
+
diff --git a/resources/js/Components/FlightsGoneBy/DepartureBoard.vue b/resources/js/Components/FlightsGoneBy/DepartureBoard.vue index d505ac4..f4ad3e3 100644 --- a/resources/js/Components/FlightsGoneBy/DepartureBoard.vue +++ b/resources/js/Components/FlightsGoneBy/DepartureBoard.vue @@ -1,9 +1,10 @@