Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions web/src/components/MotionFastestAircraft.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script>
import MotionStats from './MotionStats.svelte';
import { IconRocket } from '@tabler/icons-svelte';
import { speedUnit, formatSpeed } from '../stores/preferences';

const columns = [
$: columns = [
{ header: 'Reg', field: 'registration', class: 'font-mono whitespace-nowrap' },
{ header: 'Type', field: 'type' },
// { header: 'Flight', field: 'flight' },
{
header: 'Speed',
header: `Speed`,
field: 'ground_speed',
formatter: (value) => value ? `${value.toLocaleString()} kts` : '-'
formatter: (value) => formatSpeed(value, $speedUnit)
},
{
header: 'First Seen',
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/MotionHighestAircraft.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script>
import MotionStats from './MotionStats.svelte';
import { IconArrowUpDashed } from '@tabler/icons-svelte';
import { altitudeUnit, formatAltitude } from '../stores/preferences';

const columns = [
$: columns = [
{ header: 'Reg', field: 'registration', class: 'font-mono' },
{ header: 'Model', field: 'type' },
// { header: 'Flight', field: 'flight' },
{
header: 'Altitude',
header: `Altitude`,
field: 'barometric_altitude',
formatter: (value) => value ? `${value.toLocaleString()} ft` : '-'
formatter: (value) => formatAltitude(value, $altitudeUnit)
},
{
header: 'First Seen',
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/MotionLowestAircraft.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script>
import MotionStats from './MotionStats.svelte';
import { IconArrowDownDashed } from '@tabler/icons-svelte';
import { altitudeUnit, formatAltitude } from '../stores/preferences';

const columns = [
$: columns = [
{ header: 'Reg', field: 'registration', class: 'font-mono' },
{ header: 'Model', field: 'type' },
// { header: 'Flight', field: 'flight' },
{
header: 'Altitude',
header: `Altitude`,
field: 'barometric_altitude',
formatter: (value) => value ? `${value.toLocaleString()} ft` : '-'
formatter: (value) => formatAltitude(value, $altitudeUnit)
},
{
header: 'First Seen',
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/MotionSlowestAircraft.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script>
import MotionStats from './MotionStats.svelte';
import { IconWalk } from '@tabler/icons-svelte';
import { speedUnit, formatSpeed } from '../stores/preferences';

const columns = [
$: columns = [
{ header: 'Reg', field: 'registration', class: 'font-mono' },
{ header: 'Type', field: 'type' },
// { header: 'Flight', field: 'flight' },
{
header: 'Speed',
header: `Speed`,
field: 'ground_speed',
formatter: (value) => value ? `${value.toLocaleString()} kts` : '-'
formatter: (value) => formatSpeed(value, $speedUnit)
},
{
header: 'First Seen',
Expand Down
50 changes: 49 additions & 1 deletion web/src/components/Settings.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script>
import { onMount } from 'svelte';
import { settings } from '../stores/settings';
import { speedUnit, altitudeUnit, SPEED_UNITS, ALTITUDE_UNITS } from '../stores/preferences';
import { IconBrandGithub } from '@tabler/icons-svelte';


Expand All @@ -16,6 +17,7 @@

const menuItems = [
{ id: 'display', label: 'Display' },
{ id: 'preferences', label: 'Preferences' },
{ id: 'about', label: 'About' }
];

Expand Down Expand Up @@ -191,6 +193,50 @@
</div>
</form>

{:else if activeMenuItem === 'preferences'}
<h4 class="text-lg font-semibold mb-6">Preferences</h4>
<div class="space-y-8">

<!-- Speed Units -->
<div>
<p class="text-xl font-extralight tracking-wider mb-4">Speed Units</p>
<div class="form-control flex flex-row gap-10">
{#each Object.values(SPEED_UNITS) as unit}
<label class="label cursor-pointer justify-start gap-4">
<input
type="radio"
name="speed-unit"
class="radio radio-primary"
value={unit.value}
bind:group={$speedUnit}
/>
<span class="label-text">{unit.label} ({unit.value})</span>
</label>
{/each}
</div>
</div>

<!-- Altitude Units -->
<div>
<p class="text-xl font-extralight tracking-wider mb-4">Altitude Units</p>
<div class="form-control flex flex-row gap-10">
{#each Object.values(ALTITUDE_UNITS) as unit}
<label class="label cursor-pointer justify-start gap-4">
<input
type="radio"
name="altitude-unit"
class="radio radio-primary"
value={unit.value}
bind:group={$altitudeUnit}
/>
<span class="label-text">{unit.label} ({unit.value})</span>
</label>
{/each}
</div>
</div>

</div>

{:else if activeMenuItem === 'about'}
<div class="text-center mx-auto">
<div class="flex items-center justify-center gap-6 mb-2">
Expand All @@ -217,7 +263,9 @@
{/if}
</div>

{#if activeMenuItem !== 'about'}


{#if activeMenuItem === 'display'}
<div class="modal-action justify-end">
<button
class="btn btn-primary"
Expand Down
117 changes: 117 additions & 0 deletions web/src/stores/preferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { writable } from 'svelte/store';

/**
* Indicates if the code is running in a browser environment.
* Used to avoid errors when accessing localStorage on the server (SSR).
* @type {boolean}
*/
const browser = typeof window !== 'undefined';

/**
* Creates a Svelte store that persists its value to localStorage.
*
* If running in the browser, it attempts to read the saved value.
* It also subscribes to store changes to automatically update localStorage.
*
* @param {string} key - The unique key to save the value in localStorage.
* @param {*} initialValue - The default initial value if nothing is saved.
* @returns {import('svelte/store').Writable<*>} A writable Svelte store.
*/
const createPersistedStore = (key, initialValue) => {
// Check if we're in the browser and have a saved value
const savedValue = browser ? localStorage.getItem(key) : null;
const store = writable(savedValue || initialValue);

if (browser) {
store.subscribe((value) => {
localStorage.setItem(key, value);
});
}

return store;
};

/**
* Definition of available speed units.
* Each unit contains its display label, internal value, and conversion factor from Knots.
*
* @constant
* @type {Object.<string, {label: string, value: string, factor: number}>}
*/
export const SPEED_UNITS = {
/** Knots (Base unit for aviation) */
KTS: { label: 'Knots', value: 'kts', factor: 1 },
/** Miles per hour */
MPH: { label: 'MPH', value: 'mph', factor: 1.15078 },
/** Kilometers per hour */
KMH: { label: 'km/h', value: 'km/h', factor: 1.852 },
};

/**
* Definition of available altitude units.
* Each unit contains its label, value, and conversion factor from Feet.
*
* @constant
* @type {Object.<string, {label: string, value: string, factor: number}>}
*/
export const ALTITUDE_UNITS = {
/** Feet (Base unit for aviation) */
FT: { label: 'Feet', value: 'ft', factor: 1 },
/** Meters */
M: { label: 'Meters', value: 'm', factor: 0.3048 },
};

// The stores

/**
* Store for the user selected speed unit.
* Persists in localStorage under the key 'settings_speed_unit'.
* @type {import('svelte/store').Writable<string>}
*/
export const speedUnit = createPersistedStore('settings_speed_unit', 'kts');

/**
* Store for the user selected altitude unit.
* Persists in localStorage under the key 'settings_altitude_unit'.
* @type {import('svelte/store').Writable<string>}
*/
export const altitudeUnit = createPersistedStore('settings_altitude_unit', 'ft');


// Helpers for formatting

/**
* Formats a speed value (in knots) to the specified unit.
*
* @param {number|null} knots - The speed in knots (raw API value).
* @param {string} unitValue - The code of the desired unit (e.g. 'kts', 'mph', 'km/h').
* @returns {string} The formatted speed with its unit (e.g. "150 km/h") or "-" if value is null.
*/
export const formatSpeed = (knots, unitValue) => {
if (knots === null || knots === undefined) return '-';

// Find unit object, or default to Knots if not found
const unit = Object.values(SPEED_UNITS).find(u => u.value === unitValue) || SPEED_UNITS.KTS;
const val = knots * unit.factor;

// Format without decimals
return `${val.toLocaleString(undefined, { maximumFractionDigits: 0 })} ${unit.value}`;
};

/**
* Formats an altitude value (in feet) to the specified unit.
*
* @param {number|null} feet - The altitude in feet (raw API value).
* @param {string} unitValue - The code of the desired unit (e.g. 'ft', 'm').
* @returns {string} The formatted altitude with its unit (e.g. "1000 m") or "-" if value is null.
*/
export const formatAltitude = (feet, unitValue) => {
if (feet === null || feet === undefined) return '-';

// Find unit object, or default to Feet if not found
const unit = Object.values(ALTITUDE_UNITS).find(u => u.value === unitValue) || ALTITUDE_UNITS.FT;
const val = feet * unit.factor;

// Format without decimals
return `${val.toLocaleString(undefined, { maximumFractionDigits: 0 })} ${unit.value}`;
};