first version

This commit is contained in:
2023-01-07 10:25:40 +01:00
commit 11ec116a05
155 changed files with 4906 additions and 0 deletions

39
src/app.css Normal file
View File

@@ -0,0 +1,39 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
font-family: 'Space Mono', monospace;
height: 100%;
width: 100%;
background-color: var(--clr-bg-0);
color: var(--clr-text-0);
}
body {
padding-top: env(safe-area-inset-top);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
}
:root {
--clr-bg-0: #180b2e;
--clr-bg-1: #006ba6;
--clr-text-0: #fcfcfc;
--radius: 2rem;
--anim: all ease-in-out 200ms;
}
* {
scroll-behavior: smooth;
box-sizing: border-box;
}
svg {
height: 1em;
display: inline-block;
aspect-ratio: 1/1;
}

9
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}

16
src/app.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta
name="viewport"
content="width=device-width, viewport-fit=cover, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

253
src/fonts.css Normal file
View File

@@ -0,0 +1,253 @@
/* Space Mono */
/* cyrillic-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/2b80d9191857114935a7ddc8651898ce5c332b4b.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
font-display: swap;
font-style: normal;
font-weight: 400;
}
/* cyrillic */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/81d14acb9ab7a87e474981a7eddf71ae203f9b7e.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
font-display: swap;
font-style: normal;
font-weight: 400;
}
/* greek-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/886749ee9261d1c21938d2fc8c6ec6c3b5c3576d.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
font-display: swap;
font-style: normal;
font-weight: 400;
}
/* greek */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/3a2b22ae668071f27b7ad2bd25942f37d4336b5e.woff2) format('woff2');
unicode-range: U+0370-03FF;
font-display: swap;
font-style: normal;
font-weight: 400;
}
/* vietnamese */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/6393c1457c4d218401f198659ab88ed244671302.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
font-display: swap;
font-style: normal;
font-weight: 400;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/f5f1a3e9a8759b88f30cafc1802d64e7843991de.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
font-style: normal;
font-weight: 400;
}
/* latin */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/a435ca6d3f780680052459c8e4dda1a2ae6097e2.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
font-display: swap;
font-style: normal;
font-weight: 400;
}
/* cyrillic-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/da609c784b93e13caf638d1f12e37fba790511a7.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
font-display: swap;
font-style: normal;
font-weight: 700;
}
/* cyrillic */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/31dffb3a079789ed54432fee3bff723f92b36737.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
font-display: swap;
font-style: normal;
font-weight: 700;
}
/* greek-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/988889b2ffccfc81e6a639d6ce2509b9dc5ffa0c.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
font-display: swap;
font-style: normal;
font-weight: 700;
}
/* greek */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/6871557a261b4bd326a12b1989b91c4b86896591.woff2) format('woff2');
unicode-range: U+0370-03FF;
font-display: swap;
font-style: normal;
font-weight: 700;
}
/* vietnamese */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/8fdccb510832b50cf7c29969c65cf634b6eecce3.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
font-display: swap;
font-style: normal;
font-weight: 700;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/3a1d609e542107f59aff6b69fb94284b994dfdcb.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
font-style: normal;
font-weight: 700;
}
/* latin */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/862757cd58804ddd5677c3698cc6f660aae08958.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
font-display: swap;
font-style: normal;
font-weight: 700;
}
/* cyrillic-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/32f2ef7b1afb3cc954f8b24f74c4e43c2ca0949c.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
font-display: swap;
font-style: italic;
font-weight: 400;
}
/* cyrillic */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/3d7ef8c205a8330e6e26451051c6a5ec269a0de9.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
font-display: swap;
font-style: italic;
font-weight: 400;
}
/* greek-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/d5158e1297edbc66f3444d7389cea3ee64820094.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
font-display: swap;
font-style: italic;
font-weight: 400;
}
/* greek */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/2556c5d60452a40ff26729c27365e06e1fc4ba85.woff2) format('woff2');
unicode-range: U+0370-03FF;
font-display: swap;
font-style: italic;
font-weight: 400;
}
/* vietnamese */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/1b0a405616130035d273abf57772350c7fae36a5.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
font-display: swap;
font-style: italic;
font-weight: 400;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/6100259de9ebd81a32ebc0cc609e8553e0f133fa.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
font-style: italic;
font-weight: 400;
}
/* latin */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/7513f6e9ca633ce8ab2b87e45c14853af063936d.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
font-display: swap;
font-style: italic;
font-weight: 400;
}
/* cyrillic-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/a69f031a7a3da4af8283ad9ad2fd1bedd8d702b9.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
font-display: swap;
font-style: italic;
font-weight: 700;
}
/* cyrillic */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/4e7e2952e005602594aa39de76082c4616a1a1f6.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
font-display: swap;
font-style: italic;
font-weight: 700;
}
/* greek-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/404a7d631e3ce4d7f23e759e6161fe4dd413ed22.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
font-display: swap;
font-style: italic;
font-weight: 700;
}
/* greek */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/15e2e8e1f722cfbf268b579a29dc584bb2997351.woff2) format('woff2');
unicode-range: U+0370-03FF;
font-display: swap;
font-style: italic;
font-weight: 700;
}
/* vietnamese */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/eff94ba10e75966d058a84922705892d3532b5dc.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
font-display: swap;
font-style: italic;
font-weight: 700;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/2601cce7273ff6fd23fd00ea90e17b67590faf44.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
font-display: swap;
font-style: italic;
font-weight: 700;
}
/* latin */
@font-face {
font-family: 'Space Mono';
src: url(/fonts/0109c765e1b71c2254d207d41ada46e094d77c00.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
font-display: swap;
font-style: italic;
font-weight: 700;
}

View File

@@ -0,0 +1,14 @@
<button class="px-4 py-1 text-sm">
<slot />
</button>
<style>
button {
display: block;
border-radius: calc(var(--radius) / 2);
border: 2px solid var(--clr-bg-1);
background: hsla(0, 0%, 100%, 0.1);
box-shadow: 0 0 24px -12px #ffffff36;
width: max-content;
}
</style>

View File

@@ -0,0 +1,12 @@
<div class="px-6 py-4 w-full overflow-hidden">
<slot />
</div>
<style>
div {
border-radius: var(--radius);
border: 2px solid var(--clr-bg-1);
background: hsla(0, 0%, 100%, 0.1);
box-shadow: 0 0 24px -8px #ffffff36;
}
</style>

View File

@@ -0,0 +1,5 @@
<script lang="ts">
export let text: string
</script>
<h1 class="text-center text-5xl mb-8">{text}</h1>

View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Copy</title><rect
x="128"
y="128"
width="336"
height="336"
rx="57"
ry="57"
fill="none"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="32"
/><path
d="M383.5 128l.5-24a56.16 56.16 0 00-56-56H112a64.19 64.19 0 00-64 64v216a56.16 56.16 0 0056 56h24"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="32"
/></svg
>

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Navigate</title><path
d="M448 64L64 240.14h200a8 8 0 018 8V448z"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="32"
/></svg
>

After

Width:  |  Height:  |  Size: 288 B

View File

@@ -0,0 +1,19 @@
<script lang="ts">
export let label: string
export let value: string | undefined
export let unit: string | null = null
</script>
<div>
<div class="text-sm opacity-80">{label}</div>
<div class="text-base">
{#if value === undefined}
No data
{:else}
{value}
{/if}
{#if unit && value !== undefined}
<span class="text-xs -ml-1">{unit}</span>
{/if}
</div>
</div>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { location } from '$lib/stores/location'
import IconNavigate from './IconNavigate.svelte'
$: degree = ($location?.heading?.heading || 0) - 45
</script>
<div class="text-xl" style="transform: rotate({degree % 360}deg);">
<IconNavigate />
</div>
<style>
div {
transition: var(--anim);
}
</style>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
export let value: string
export let values: { label: string; value: string }[]
</script>
<select bind:value class="px-4 py-1 text-sm">
{#each values as { label, value }}
<option {value}>{label}</option>
{/each}
</select>
<style>
select {
display: block;
border-radius: calc(var(--radius) / 2);
border: 2px solid var(--clr-bg-1);
background: hsla(0, 0%, 100%, 0.1);
box-shadow: 0 0 24px -12px #ffffff36;
width: max-content;
}
</style>

View File

@@ -0,0 +1,49 @@
<script lang="ts">
export let value: boolean
</script>
<label class="switch">
<input type="checkbox" bind:checked={value} />
<span class="slider" />
</label>
<style>
.switch {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
visibility: hidden;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: var(--radius);
border: 2px solid var(--clr-bg-1);
background-color: var(--clr-bg-0);
transition: var(--anim);
}
.slider:before {
position: absolute;
content: '';
height: calc(100% - 0.25rem);
aspect-ratio: 1/1;
left: 0.125rem;
bottom: 0.125rem;
background-color: var(--clr-bg-1);
transition: var(--anim);
border-radius: var(--radius);
}
input:checked + .slider:before {
transform: translateX(calc(1.5rem));
}
</style>

View File

@@ -0,0 +1,6 @@
<script lang="ts">
</script>
<div class="grid grid-cols-1 gap-4">
<slot />
</div>

88
src/lib/geo.ts Normal file
View File

@@ -0,0 +1,88 @@
export enum CoordinateType {
Decimal = 'DECIMAL',
DMS = 'DMS',
}
export enum CoordinateLink {
GoogleMaps = 'GOOGLE_MAPS',
OpenStreetMap = 'OpenStreetMap',
}
export class Coordinates {
constructor(private lat: number, private lon: number) {}
private static format(value: number, type: CoordinateType): string {
switch (type) {
case CoordinateType.DMS: {
// https://en.wikipedia.org/wiki/Decimal_degrees#Example
const degree = Math.trunc(value)
const remainder = Math.abs(value - degree)
const minute = Math.trunc(60 * remainder)
const second = 3600 * remainder - 60 * minute
const d = degree.toFixed(0).padStart(3, '0')
const m = minute.toFixed(0).padStart(2, '0')
const s = second.toFixed(6).padStart(2, '0')
return `${d}°${m}${s}`
}
case CoordinateType.Decimal: {
return value.toFixed(7)
}
}
}
format(type: CoordinateType, bearing: boolean): { lat: string; lon: string } {
const lat = Coordinates.format(this.lat, type)
const lon = Coordinates.format(this.lon, type)
if (bearing) {
return {
lat: `${lat.replace('-', '')} ${this.lat < 0 ? 'S' : 'N'}`,
lon: `${lon.replace('-', '')} ${this.lon < 0 ? 'W' : 'E'}`,
}
}
return { lat, lon }
}
link(type: CoordinateLink): string {
switch (type) {
case CoordinateLink.GoogleMaps:
return `https://www.google.com/maps/place/${this.lat},${this.lon}`
case CoordinateLink.OpenStreetMap:
return `https://www.openstreetmap.org/?mlat=${this.lat}&mlon=${this.lon}`
}
}
}
export class Heading {
private static Bearings = {
N: 0,
NE: 45,
E: 90,
SE: 135,
S: 180,
SW: 225,
W: 270,
NW: 315,
}
constructor(public heading: number) {}
format(): string {
return `${this.heading.toFixed(0)}° ${this.bearing()}`
}
bearing(): string {
let bearing = ''
let min = 360
for (const [b, degree] of Object.entries(Heading.Bearings)) {
const diff = Math.abs(this.heading - degree)
if (diff < min) {
min = diff
bearing = b
}
}
return bearing
}
}

6
src/lib/stores/app.ts Normal file
View File

@@ -0,0 +1,6 @@
import { App, type AppInfo } from '@capacitor/app'
import { readable } from 'svelte/store'
export const app = readable<AppInfo | null>(null, (set) => {
App.getInfo().then((info) => set(info))
})

View File

@@ -0,0 +1,49 @@
import { Coordinates, Heading } from '$lib/geo'
import { Geolocation } from '@capacitor/geolocation'
import { readable } from 'svelte/store'
type NullableNumber = number | null
type Location = {
coords: Coordinates
altitude: NullableNumber
accuracy: {
coords: NullableNumber
altitude: NullableNumber
}
speed: NullableNumber
heading: Heading | null
time: {
stamp: Date
fix: number
}
}
let last = Date.now()
export const location = readable<Location | null>(null, (set) => {
let id: string
Geolocation.watchPosition({ enableHighAccuracy: true }, (p) => {
if (p) {
const now = Date.now()
const location: Location = {
coords: new Coordinates(p.coords.latitude, p.coords.longitude),
altitude: p.coords.altitude,
accuracy: {
altitude: p.coords.altitudeAccuracy ?? null,
coords: p.coords.accuracy,
},
speed: p.coords.speed,
heading: p.coords.heading ? new Heading(p.coords.heading) : null,
time: {
stamp: new Date(p.timestamp),
fix: now - last,
},
}
last = now
set(location)
}
}).then((i) => (id = i))
return () => {
if (id) Geolocation.clearWatch({ id })
}
})

View File

@@ -0,0 +1,30 @@
import { CoordinateType } from '$lib/geo'
import { Preferences } from '@capacitor/preferences'
import { writable } from 'svelte/store'
export enum Unit {
Metric = 'METRIC',
Imperial = 'IMPERIAL',
}
const defaults = {
bearings: true,
type: CoordinateType.DMS,
unit: Unit.Metric,
}
type Defaults = typeof defaults
export function setting<K extends keyof Defaults>(key: K) {
const store = writable<Defaults[K]>(defaults[key])
Preferences.get({ key }).then(({ value }) => {
store.set(value ? JSON.parse(value) : defaults[key])
store.subscribe((value) => {
Preferences.set({ key, value: JSON.stringify(value) })
})
})
return store
}
export const type = setting('type')
export const bearings = setting('bearings')
export const unit = setting('unit')

98
src/lib/views/Home.svelte Normal file
View File

@@ -0,0 +1,98 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte'
import Card from '$lib/components/Card.svelte'
import H1 from '$lib/components/H1.svelte'
import Measurement from '$lib/components/Measurement.svelte'
import Needle from '$lib/components/Needle.svelte'
import VerticalGrid from '$lib/components/VerticalGrid.svelte'
import { CoordinateLink } from '$lib/geo'
import { app } from '$lib/stores/app'
import { location } from '$lib/stores/location'
import { bearings, type } from '$lib/stores/settings'
import Settings from './Settings.svelte'
function pad(value: number): string {
return value.toString().padStart(2, '0')
}
$: stamp = $location?.time.stamp
$: date = stamp && `${pad(stamp.getHours())}:${pad(stamp.getMinutes())}:${pad(stamp.getSeconds())}`
$: coords = $location?.coords.format($type, $bearings)
$: fix = $location?.time.fix.toFixed(0)
</script>
<H1 text="GPS Info" />
<VerticalGrid>
<Card>
<VerticalGrid>
<div class="flex items-center">
<div class="flex-grow">
<Measurement label="Heading" value={$location?.heading?.format()} />
</div>
<Needle />
</div>
<Measurement label="Latitude" value={coords?.lat} />
<Measurement label="Longitude" value={coords?.lon} />
<div>
<span class="text-sm italic">Open in:</span>
<div class="mt-1 grid gap-2 grid-flow-col auto-cols-max overflow-auto">
<a href={$location?.coords.link(CoordinateLink.GoogleMaps)} target="_blank" rel="noopener noreferrer">
<Button>Google Maps</Button>
</a>
<a href={$location?.coords.link(CoordinateLink.OpenStreetMap)} target="_blank" rel="noopener noreferrer">
<Button>OpenStreetMap</Button>
</a>
</div>
</div>
</VerticalGrid>
</Card>
<Card>
<VerticalGrid>
<div class="grid gap-4 grid-cols-2">
<Measurement label="Altitude" value={$location?.altitude?.toFixed(2)} unit="m.a.s.l." />
<Measurement label="Speed" value={$location?.speed?.toFixed(2)} unit="m/s" />
</div>
<div>
<span class="text-sm italic">Accuracy</span>
<div class="grid gap-4 grid-cols-2">
<Measurement label="Vertical" value={$location?.accuracy.altitude?.toFixed(1)} unit="m" />
<Measurement label="Horizontal" value={$location?.accuracy.coords?.toFixed(1)} unit="m" />
</div>
</div>
<div>
<span class="text-sm italic">Fixing</span>
<div class="grid gap-4 grid-cols-2">
<Measurement label="Time" value={fix} unit="mnarrows" />
<Measurement label="Last" value={date || undefined} />
</div>
</div>
</VerticalGrid>
</Card>
<Settings />
<Card>
<VerticalGrid>
<span class="text-sm italic">About</span>
<p>
This is Open Source software.
<a class="italic" href="https://github.com/cupcakearmy/gps-info" target="_blank" rel="noopener noreferrer"
>👉 Source code</a
>
</p>
<p>
If you have issues / suggestions feel free to report them <a
class="italic"
href="https://github.com/cupcakearmy/gps-info/issues"
target="_blank"
rel="noopener noreferrer">here</a
>.
</p>
<p>Version: {$app?.version}</p>
</VerticalGrid>
</Card>
</VerticalGrid>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import Card from '$lib/components/Card.svelte'
import Select from '$lib/components/Select.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import VerticalGrid from '$lib/components/VerticalGrid.svelte'
import { CoordinateType } from '$lib/geo'
import { bearings, type } from '$lib/stores/settings'
</script>
<Card>
<span class="text-sm italic">Settings</span>
<VerticalGrid>
<div class="flex items-center">
<div class="flex-auto">GPS format</div>
<div>
<Select bind:value={$type} values={Object.values(CoordinateType).map((v) => ({ label: v, value: v }))} />
</div>
</div>
<div class="flex items-center">
<div class="flex-auto">Use bearings</div>
<Toggle bind:value={$bearings} />
</div>
</VerticalGrid>
</Card>

View File

@@ -0,0 +1,8 @@
<script lang="ts">
import '../app.css'
import '../fonts.css'
</script>
<div class="px-8 py-16">
<slot />
</div>

1
src/routes/+layout.ts Normal file
View File

@@ -0,0 +1 @@
export const ssr = false

5
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,5 @@
<script lang="ts">
import Home from '$lib/views/Home.svelte'
</script>
<Home />