style: run prettier
All checks were successful
TrafficCue CI / check (push) Successful in 1m34s
TrafficCue CI / build (push) Successful in 10m30s
TrafficCue CI / build-android (push) Successful in 26m47s

This commit is contained in:
2025-10-21 15:46:50 +02:00
parent 4036790a4b
commit 372b31876d
17 changed files with 306 additions and 167 deletions

View File

@ -55,13 +55,22 @@
preferredFuel: "Diesel", preferredFuel: "Diesel",
}); });
const biggerButtonsInDrive = localStore<boolean>("bigger-buttons-in-drive", false); const biggerButtonsInDrive = localStore<boolean>(
const shouldUseLargeSize = $derived(biggerButtonsInDrive.current ? isDriving() : false); "bigger-buttons-in-drive",
false,
);
const shouldUseLargeSize = $derived(
biggerButtonsInDrive.current ? isDriving() : false,
);
</script> </script>
<Drawer.Root bind:open> <Drawer.Root bind:open>
<Drawer.Trigger <Drawer.Trigger
class={buttonVariants({ variant: "secondary", class: "w-full", size: shouldUseLargeSize ? "drive" : "default" })} class={buttonVariants({
variant: "secondary",
class: "w-full",
size: shouldUseLargeSize ? "drive" : "default",
})}
> >
{@render children()} {@render children()}
</Drawer.Trigger> </Drawer.Trigger>

View File

@ -178,8 +178,13 @@
{/if} {/if}
{#if !hideSearch && !!location.speed != false} {#if !hideSearch && !!location.speed != false}
<div id="speedometer" style="position: fixed; {mobileView ? `bottom: calc(50px + ${sidebarHeight.current}px + 10px); right: 10px;` : "bottom: 10px; right: 10px;"}"> <div
{(location.speed * 3.6 | 0).toFixed(0)} id="speedometer"
style="position: fixed; {mobileView
? `bottom: calc(50px + ${sidebarHeight.current}px + 10px); right: 10px;`
: 'bottom: 10px; right: 10px;'}"
>
{((location.speed * 3.6) | 0).toFixed(0)}
</div> </div>
{/if} {/if}
{#if getRoadMetadata().current} {#if getRoadMetadata().current}
@ -187,7 +192,12 @@
{#if meta.maxspeed} {#if meta.maxspeed}
{@const maxspeed = getSpeed(meta.maxspeed)} {@const maxspeed = getSpeed(meta.maxspeed)}
{#if maxspeed && maxspeed < 100} {#if maxspeed && maxspeed < 100}
<div id="max-speed" style="position: fixed; {mobileView ? `bottom: calc(50px + ${sidebarHeight.current}px + 10px + 2ch + 20px + 10px); right: 10px;` : "bottom: calc(10px + 2ch + 20px + 10px); right: 10px;"}"> <div
id="max-speed"
style="position: fixed; {mobileView
? `bottom: calc(50px + ${sidebarHeight.current}px + 10px + 2ch + 20px + 10px); right: 10px;`
: 'bottom: calc(10px + 2ch + 20px + 10px); right: 10px;'}"
>
{meta.maxspeed} {meta.maxspeed}
</div> </div>
{/if} {/if}

View File

@ -41,13 +41,22 @@
} }
} }
const biggerButtonsInDrive = localStore<boolean>("bigger-buttons-in-drive", false); const biggerButtonsInDrive = localStore<boolean>(
const shouldUseLargeSize = $derived(biggerButtonsInDrive.current ? isDriving() : false); "bigger-buttons-in-drive",
false,
);
const shouldUseLargeSize = $derived(
biggerButtonsInDrive.current ? isDriving() : false,
);
</script> </script>
<Drawer.Root bind:open> <Drawer.Root bind:open>
<Drawer.Trigger <Drawer.Trigger
class={buttonVariants({ variant: "secondary", class: "w-full", size: shouldUseLargeSize ? "drive" : "default" })} class={buttonVariants({
variant: "secondary",
class: "w-full",
size: shouldUseLargeSize ? "drive" : "default",
})}
> >
{@const vehicle = selectedVehicle()?.data ?? DefaultVehicle} {@const vehicle = selectedVehicle()?.data ?? DefaultVehicle}
{@const Icon = getVehicleIcon(vehicle.type)} {@const Icon = getVehicleIcon(vehicle.type)}

View File

@ -35,13 +35,15 @@ export const location = $state({
lastUpdate: null as Date | null, lastUpdate: null as Date | null,
}); });
const _isDriving = $derived(location.speed > (7 / 3.6)); const _isDriving = $derived(location.speed > 7 / 3.6);
export function isDriving() { export function isDriving() {
return _isDriving; return _isDriving;
} }
const roadMetadata: WrappedValue<GeoJSON.GeoJsonProperties> = $state({ current: null }); const roadMetadata: WrappedValue<GeoJSON.GeoJsonProperties> = $state({
current: null,
});
export function getRoadMetadata() { export function getRoadMetadata() {
return roadMetadata; return roadMetadata;
@ -60,8 +62,11 @@ export function watchLocation() {
location.available = true; location.available = true;
location.heading = pos.coords.heading; location.heading = pos.coords.heading;
location.lastUpdate = new Date(); location.lastUpdate = new Date();
getMeta({ lat: location.lat, lon: location.lng }, "transportation").then((meta) => { getMeta(
{ lat: location.lat, lon: location.lng },
"transportation",
).then((meta) => {
roadMetadata.current = meta; roadMetadata.current = meta;
}); });

View File

@ -1,7 +1,12 @@
<script lang="ts"> <script lang="ts">
import Button from "$lib/components/ui/button/button.svelte"; import Button from "$lib/components/ui/button/button.svelte";
import * as Card from "$lib/components/ui/card"; import * as Card from "$lib/components/ui/card";
import { fetchEvents, type DAVCalendar, type DAVCredentials, type DAVEvent } from "$lib/services/CalDAV"; import {
fetchEvents,
type DAVCalendar,
type DAVCredentials,
type DAVEvent,
} from "$lib/services/CalDAV";
import { search } from "$lib/services/Search"; import { search } from "$lib/services/Search";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { map, pin } from "../map.svelte"; import { map, pin } from "../map.svelte";
@ -11,13 +16,14 @@
onMount(async () => { onMount(async () => {
const calendars = localStorage.getItem("calendars"); const calendars = localStorage.getItem("calendars");
if(!calendars) return; if (!calendars) return;
const parsedCalendars: (DAVCalendar & { credentials: DAVCredentials })[] = JSON.parse(calendars); const parsedCalendars: (DAVCalendar & { credentials: DAVCredentials })[] =
for(const calendar of parsedCalendars) { JSON.parse(calendars);
for (const calendar of parsedCalendars) {
const calendarEvents = await fetchEvents(calendar, calendar.credentials); const calendarEvents = await fetchEvents(calendar, calendar.credentials);
events.push(...calendarEvents); events.push(...calendarEvents);
} }
}) });
</script> </script>
<div id="events"> <div id="events">
@ -31,35 +37,40 @@
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<p> <p>
<strong>{m["calendar.start"]()}:</strong> {event.start?.toLocaleString("de-DE") || m["calendar.no-start"]()} <strong>{m["calendar.start"]()}:</strong>
{event.start?.toLocaleString("de-DE") || m["calendar.no-start"]()}
</p> </p>
<p> <p>
<strong>{m["calendar.end"]()}:</strong> {event.end?.toLocaleString("de-DE") || m["calendar.no-end"]()} <strong>{m["calendar.end"]()}:</strong>
{event.end?.toLocaleString("de-DE") || m["calendar.no-end"]()}
</p> </p>
<p> <p>
{#if event.location} {#if event.location}
<strong>{m["calendar.location"]()}:</strong> {event.location} <strong>{m["calendar.location"]()}:</strong> {event.location}
{:else} {:else}
<strong>{m["calendar.location"]()}:</strong> {m["calendar.no-location"]()} <strong>{m["calendar.location"]()}:</strong>
{m["calendar.no-location"]()}
{/if} {/if}
</p> </p>
</Card.Content> </Card.Content>
<Card.Footer> <Card.Footer>
<Button onclick={async () => { <Button
if(!event.location) return; onclick={async () => {
const features = await search(event.location, 0, 0); if (!event.location) return;
if(features.length == 0) { const features = await search(event.location, 0, 0);
return void alert("Can't find location"); if (features.length == 0) {
} return void alert("Can't find location");
const lat = features[0].geometry.coordinates[1]; }
const lng = features[0].geometry.coordinates[0]; const lat = features[0].geometry.coordinates[1];
pin.dropPin(lat, lng); const lng = features[0].geometry.coordinates[0];
pin.showInfo(); pin.dropPin(lat, lng);
map.value?.flyTo({ pin.showInfo();
center: [lng, lat], map.value?.flyTo({
zoom: 19, center: [lng, lat],
}); zoom: 19,
}}>{m.open()}</Button> });
}}>{m.open()}</Button
>
</Card.Footer> </Card.Footer>
</Card.Root> </Card.Root>
{/each} {/each}

View File

@ -74,11 +74,13 @@
const FROM: WorldLocation = const FROM: WorldLocation =
fromLocation == "current" fromLocation == "current"
? { lat: location.lat, lon: location.lng } ? { lat: location.lat, lon: location.lng }
: savedLocations.current.find(s => s.name == fromLocation) : savedLocations.current.find((s) => s.name == fromLocation)
? { ? {
lat: savedLocations.current.find(s => s.name == fromLocation)!.data.lat, lat: savedLocations.current.find((s) => s.name == fromLocation)!
lon: savedLocations.current.find(s => s.name == fromLocation)!.data.lng, .data.lat,
} lon: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lng,
}
: { : {
lat: parseFloat(fromLocation.split(",")[0]), lat: parseFloat(fromLocation.split(",")[0]),
lon: parseFloat(fromLocation.split(",")[1]), lon: parseFloat(fromLocation.split(",")[1]),
@ -86,11 +88,13 @@
const TO: WorldLocation = const TO: WorldLocation =
toLocation == "current" toLocation == "current"
? { lat: location.lat, lon: location.lng } ? { lat: location.lat, lon: location.lng }
: savedLocations.current.find(s => s.name == toLocation) : savedLocations.current.find((s) => s.name == toLocation)
? { ? {
lat: savedLocations.current.find(s => s.name == fromLocation)!.data.lat, lat: savedLocations.current.find((s) => s.name == fromLocation)!
lon: savedLocations.current.find(s => s.name == fromLocation)!.data.lng, .data.lat,
} lon: savedLocations.current.find((s) => s.name == fromLocation)!
.data.lng,
}
: { : {
lat: parseFloat(toLocation.split(",")[0]), lat: parseFloat(toLocation.split(",")[0]),
lon: parseFloat(toLocation.split(",")[1]), lon: parseFloat(toLocation.split(",")[1]),

View File

@ -8,4 +8,7 @@
{m["sidebar.appearance.header"]()} {m["sidebar.appearance.header"]()}
</SidebarHeader> </SidebarHeader>
<SettingsToggle text={m["sidebar.appearance.bigger-buttons-in-drive"]()} localStorageKey="bigger-buttons-in-drive" /> <SettingsToggle
text={m["sidebar.appearance.bigger-buttons-in-drive"]()}
localStorageKey="bigger-buttons-in-drive"
/>

View File

@ -6,10 +6,16 @@
import * as Drawer from "$lib/components/ui/drawer"; import * as Drawer from "$lib/components/ui/drawer";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import Input from "$lib/components/ui/input/input.svelte"; import Input from "$lib/components/ui/input/input.svelte";
import { fetchCalendars, findScheme, type AuthScheme, type DAVCalendar, type DAVCredentials } from "$lib/services/CalDAV"; import {
fetchCalendars,
findScheme,
type AuthScheme,
type DAVCalendar,
type DAVCredentials,
} from "$lib/services/CalDAV";
import { onMount } from "svelte"; import { onMount } from "svelte";
let calendars: (DAVCalendar & {credentials: DAVCredentials})[] = $state([]); let calendars: (DAVCalendar & { credentials: DAVCredentials })[] = $state([]);
let calDavDrawerOpen = $state(false); let calDavDrawerOpen = $state(false);
let calDavLoading = $state(false); let calDavLoading = $state(false);
@ -25,15 +31,19 @@
calDavState = m["sidebar.calendar.probing-server"](); calDavState = m["sidebar.calendar.probing-server"]();
calDavScheme = await findScheme(calDavUrl); calDavScheme = await findScheme(calDavUrl);
calDavState = m["sidebar.calendar.discovering-calendars"](); calDavState = m["sidebar.calendar.discovering-calendars"]();
calDavCalendars = await fetchCalendars(calDavUrl, { scheme: calDavScheme, username: calDavUsername, password: calDavPassword }); calDavCalendars = await fetchCalendars(calDavUrl, {
scheme: calDavScheme,
username: calDavUsername,
password: calDavPassword,
});
calDavState = ""; calDavState = "";
} }
onMount(() => { onMount(() => {
if(localStorage.getItem("calendars")) { if (localStorage.getItem("calendars")) {
calendars = JSON.parse(localStorage.getItem("calendars")!); calendars = JSON.parse(localStorage.getItem("calendars")!);
} }
}) });
</script> </script>
<SidebarHeader> <SidebarHeader>
@ -41,15 +51,30 @@
</SidebarHeader> </SidebarHeader>
<Drawer.Root bind:open={calDavDrawerOpen}> <Drawer.Root bind:open={calDavDrawerOpen}>
<Drawer.Content> <Drawer.Content>
<Drawer.Header> <Drawer.Header>
<Drawer.Title>{m["sidebar.calendar.connect"]()}</Drawer.Title> <Drawer.Title>{m["sidebar.calendar.connect"]()}</Drawer.Title>
</Drawer.Header> </Drawer.Header>
<div class="p-4 pt-0 flex flex-col gap-2"> <div class="p-4 pt-0 flex flex-col gap-2">
{#if calDavCalendars} {#if calDavCalendars}
<Input type="url" placeholder="https://my-caldav-server.com/..." disabled={calDavLoading} bind:value={calDavUrl} /> <Input
<Input type="text" placeholder="Username" disabled={calDavLoading} bind:value={calDavUsername} /> type="url"
<Input type="password" placeholder="Password" disabled={calDavLoading} bind:value={calDavPassword} /> placeholder="https://my-caldav-server.com/..."
disabled={calDavLoading}
bind:value={calDavUrl}
/>
<Input
type="text"
placeholder="Username"
disabled={calDavLoading}
bind:value={calDavUsername}
/>
<Input
type="password"
placeholder="Password"
disabled={calDavLoading}
bind:value={calDavPassword}
/>
{#if calDavState} {#if calDavState}
<span>{calDavState}</span> <span>{calDavState}</span>
{/if} {/if}
@ -59,40 +84,51 @@
<ul class="max-h-48 overflow-y-auto"> <ul class="max-h-48 overflow-y-auto">
{#each calDavCalendars as calendar (calendar.url)} {#each calDavCalendars as calendar (calendar.url)}
<li> <li>
<Button class="w-full" variant="secondary" onclick={() => { <Button
if(localStorage.getItem("calendars")) { class="w-full"
const existing = JSON.parse(localStorage.getItem("calendars")!); variant="secondary"
existing.push({ onclick={() => {
name: calendar.name, if (localStorage.getItem("calendars")) {
url: calendar.url, const existing = JSON.parse(
credentials: { localStorage.getItem("calendars")!,
username: calDavUsername, );
password: calDavPassword, existing.push({
scheme: calDavScheme name: calendar.name,
} url: calendar.url,
}); credentials: {
localStorage.setItem("calendars", JSON.stringify(existing)); username: calDavUsername,
} else { password: calDavPassword,
localStorage.setItem("calendars", JSON.stringify([{ scheme: calDavScheme,
name: calendar.name, },
url: calendar.url, });
credentials: { localStorage.setItem("calendars", JSON.stringify(existing));
username: calDavUsername, } else {
password: calDavPassword, localStorage.setItem(
scheme: calDavScheme "calendars",
} JSON.stringify([
}])); {
} name: calendar.name,
calendars.push({ url: calendar.url,
name: calendar.name, credentials: {
url: calendar.url, username: calDavUsername,
credentials: { password: calDavPassword,
username: calDavUsername, scheme: calDavScheme,
password: calDavPassword, },
scheme: calDavScheme },
]),
);
} }
}); calendars.push({
}}> name: calendar.name,
url: calendar.url,
credentials: {
username: calDavUsername,
password: calDavPassword,
scheme: calDavScheme,
},
});
}}
>
{calendar.name} {calendar.name}
</Button> </Button>
</li> </li>
@ -100,33 +136,44 @@
</ul> </ul>
{/if} {/if}
</div> </div>
<Drawer.Footer> <Drawer.Footer>
{#if calDavCalendars.length === 0} {#if calDavCalendars.length === 0}
<Button onclick={async () => { <Button
calDavLoading = true; onclick={async () => {
await fetchCalDav().catch((e) => { calDavLoading = true;
calDavState = e; await fetchCalDav().catch((e) => {
calDavLoading = false; calDavState = e;
}) calDavLoading = false;
}}>{m.submit()}</Button> });
}}>{m.submit()}</Button
>
{/if} {/if}
<Drawer.Close> <Drawer.Close>
{calDavCalendars.length === 0 ? m.done() : m.cancel()} {calDavCalendars.length === 0 ? m.done() : m.cancel()}
</Drawer.Close> </Drawer.Close>
</Drawer.Footer> </Drawer.Footer>
</Drawer.Content> </Drawer.Content>
</Drawer.Root> </Drawer.Root>
{#each calendars as calendar (calendar.url)} {#each calendars as calendar (calendar.url)}
<div class="p-2 border rounded mb-2"> <div class="p-2 border rounded mb-2">
<h3 class="font-medium">{calendar.name}</h3> <h3 class="font-medium">{calendar.name}</h3>
<Button variant="destructive" size="sm" class="mt-2" onclick={() => { <Button
calendars = calendars.filter(c => c.url !== calendar.url); variant="destructive"
localStorage.setItem("calendars", JSON.stringify(calendars)); size="sm"
}}>{m.delete()}</Button> class="mt-2"
onclick={() => {
calendars = calendars.filter((c) => c.url !== calendar.url);
localStorage.setItem("calendars", JSON.stringify(calendars));
}}>{m.delete()}</Button
>
</div> </div>
{/each} {/each}
<SettingsButton text={m["sidebar.calendar.connect"]()} icon={CalendarPlusIcon} onclick={() => { <SettingsButton
calDavDrawerOpen = true; text={m["sidebar.calendar.connect"]()}
}} /> icon={CalendarPlusIcon}
onclick={() => {
calDavDrawerOpen = true;
}}
/>

View File

@ -1,6 +1,6 @@
<script> <script>
import { import {
CalendarSearchIcon, CalendarSearchIcon,
CloudUploadIcon, CloudUploadIcon,
HandIcon, HandIcon,
MapIcon, MapIcon,
@ -62,20 +62,21 @@
onclick={async () => { onclick={async () => {
const url = prompt("URL?"); const url = prompt("URL?");
if (!url) return; if (!url) return;
const scheme = await invoke("dav_find_scheme", { url }) const scheme = await invoke("dav_find_scheme", { url }).catch((err) => {
.catch((err) => { alert("Error fetching scheme: " + err);
alert("Error fetching scheme: " + err); });
});
alert("Found scheme: " + scheme); alert("Found scheme: " + scheme);
const username = prompt("Username?"); const username = prompt("Username?");
const password = prompt("Password?"); const password = prompt("Password?");
if (!username || !password) return; if (!username || !password) return;
const credentials = { scheme, username, password }; const credentials = { scheme, username, password };
invoke("dav_fetch_calendars", { url, credentials }).then((calendars) => { invoke("dav_fetch_calendars", { url, credentials })
alert("Fetched calendars: " + JSON.stringify(calendars)); .then((calendars) => {
}).catch((err) => { alert("Fetched calendars: " + JSON.stringify(calendars));
alert("Error fetching calendars: " + err); })
}); .catch((err) => {
alert("Error fetching calendars: " + err);
});
}} }}
/> />
<SettingsButton <SettingsButton
@ -88,11 +89,16 @@
const password = prompt("Password?"); const password = prompt("Password?");
if (!url || !username || !password) return; if (!url || !username || !password) return;
const credentials = { scheme, username, password }; const credentials = { scheme, username, password };
invoke("dav_fetch_events", { calendar: { name: "Calendar", url }, credentials }).then((events) => { invoke("dav_fetch_events", {
alert("Fetched events: " + JSON.stringify(events)); calendar: { name: "Calendar", url },
}).catch((err) => { credentials,
alert("Error fetching events: " + err); })
}); .then((events) => {
alert("Fetched events: " + JSON.stringify(events));
})
.catch((err) => {
alert("Error fetching events: " + err);
});
}} }}
/> />
</section> </section>

View File

@ -31,7 +31,7 @@
view="appearance" view="appearance"
/> />
</section> </section>
<section> <section>
<h2>{m["sidebar.settings.connections"]()}</h2> <h2>{m["sidebar.settings.connections"]()}</h2>
<SettingsButton <SettingsButton

View File

@ -11,7 +11,11 @@
onchange, onchange,
disabled, disabled,
localStorageKey, localStorageKey,
value = $bindable(localStorageKey ? localStorage.getItem(localStorageKey) === "true" : false), value = $bindable(
localStorageKey
? localStorage.getItem(localStorageKey) === "true"
: false,
),
}: { }: {
icon?: Component<IconProps>; icon?: Component<IconProps>;
text: string; text: string;
@ -23,20 +27,26 @@
</script> </script>
<div class="flex"> <div class="flex">
<Label <Label style="width: 100%;" for="settings-toggle">
style="width: 100%;"
for="settings-toggle"
>
{#if Icon} {#if Icon}
<Icon /> <Icon />
{/if} {/if}
{text} {text}
</Label> </Label>
<Switch {disabled} bind:checked={value} onCheckedChange={() => { <Switch
if (onchange) onchange(); {disabled}
if (localStorageKey) { bind:checked={value}
localStorage.setItem(localStorageKey, value ? "true" : "false"); onCheckedChange={() => {
eventTarget.dispatchEvent(new CustomEvent("localStorageChanged", { detail: { key: localStorageKey, value } })); if (onchange) onchange();
} if (localStorageKey) {
}} id="settings-toggle" /> localStorage.setItem(localStorageKey, value ? "true" : "false");
eventTarget.dispatchEvent(
new CustomEvent("localStorageChanged", {
detail: { key: localStorageKey, value },
}),
);
}
}}
id="settings-toggle"
/>
</div> </div>

View File

@ -8,8 +8,13 @@
} from "svelte/elements"; } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
const biggerButtonsInDrive = localStore<boolean>("bigger-buttons-in-drive", false); const biggerButtonsInDrive = localStore<boolean>(
const shouldUseLargeSize = $derived(biggerButtonsInDrive.current ? isDriving() : false); "bigger-buttons-in-drive",
false,
);
const shouldUseLargeSize = $derived(
biggerButtonsInDrive.current ? isDriving() : false,
);
export const buttonVariants = tv({ export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
@ -32,7 +37,7 @@
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9", icon: "size-9",
drive: "h-12 rounded-md px-8 has-[>svg]:px-6" drive: "h-12 rounded-md px-8 has-[>svg]:px-6",
}, },
}, },
defaultVariants: { defaultVariants: {
@ -69,7 +74,10 @@
<a <a
bind:this={ref} bind:this={ref}
data-slot="button" data-slot="button"
class={cn(buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }), className)} class={cn(
buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }),
className,
)}
href={disabled ? undefined : href} href={disabled ? undefined : href}
aria-disabled={disabled} aria-disabled={disabled}
role={disabled ? "link" : undefined} role={disabled ? "link" : undefined}
@ -82,7 +90,10 @@
<button <button
bind:this={ref} bind:this={ref}
data-slot="button" data-slot="button"
class={cn(buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }), className)} class={cn(
buttonVariants({ variant, size: shouldUseLargeSize ? "drive" : size }),
className,
)}
{type} {type}
{disabled} {disabled}
{...restProps} {...restProps}

View File

@ -14,7 +14,7 @@
data-slot="label" data-slot="label"
class={cn( class={cn(
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50", "flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
className className,
)} )}
{...restProps} {...restProps}
/> />

View File

@ -16,14 +16,14 @@
data-slot="switch" data-slot="switch"
class={cn( class={cn(
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 shadow-xs peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 shadow-xs peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...restProps} {...restProps}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
class={cn( class={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0" "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)} )}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>

View File

@ -22,18 +22,24 @@ export interface DAVEvent {
} }
export async function findScheme(url: string): Promise<AuthScheme> { export async function findScheme(url: string): Promise<AuthScheme> {
const scheme = await invoke("dav_find_scheme", { url }); const scheme = await invoke("dav_find_scheme", { url });
return scheme as AuthScheme; return scheme as AuthScheme;
} }
export async function fetchCalendars(url: string, credentials: DAVCredentials): Promise<DAVCalendar[]> { export async function fetchCalendars(
url: string,
credentials: DAVCredentials,
): Promise<DAVCalendar[]> {
const calendars = await invoke("dav_fetch_calendars", { url, credentials }); const calendars = await invoke("dav_fetch_calendars", { url, credentials });
return calendars as DAVCalendar[]; return calendars as DAVCalendar[];
} }
export async function fetchEvents(calendar: DAVCalendar, credentials: DAVCredentials): Promise<DAVEvent[]> { export async function fetchEvents(
calendar: DAVCalendar,
credentials: DAVCredentials,
): Promise<DAVEvent[]> {
const events = await invoke("dav_fetch_events", { calendar, credentials }); const events = await invoke("dav_fetch_events", { calendar, credentials });
return (events as DAVEvent[]).map(e => { return (events as DAVEvent[]).map((e) => {
if (e.start) e.start = new Date(e.start); if (e.start) e.start = new Date(e.start);
if (e.end) e.end = new Date(e.end); if (e.end) e.end = new Date(e.end);
return e; return e;

View File

@ -11,8 +11,8 @@ function getFeatureDistance(f: GeoJSON.Feature, point: [number, number]) {
// Compute the min distance across all parts // Compute the min distance across all parts
return Math.min( return Math.min(
...f.geometry.coordinates.map((coords) => ...f.geometry.coordinates.map((coords) =>
pointToLineDistance(point, lineString(coords)) pointToLineDistance(point, lineString(coords)),
) ),
); );
} else { } else {
return Infinity; return Infinity;
@ -30,8 +30,11 @@ export async function getMeta(coord: WorldLocation, layer: string) {
const feature = layerData.feature(i); const feature = layerData.feature(i);
features.push(feature.toGeoJSON(zxy.x, zxy.y, zxy.z)); features.push(feature.toGeoJSON(zxy.x, zxy.y, zxy.z));
} }
const filtered = features.filter((f) => f.geometry.type === "LineString" || f.geometry.type == "MultiLineString"); const filtered = features.filter(
if(filtered.length === 0) return null; (f) =>
f.geometry.type === "LineString" || f.geometry.type == "MultiLineString",
);
if (filtered.length === 0) return null;
const nearest = filtered.reduce((a, b) => { const nearest = filtered.reduce((a, b) => {
const distA = getFeatureDistance(a, [coord.lon, coord.lat]); const distA = getFeatureDistance(a, [coord.lon, coord.lat]);
const distB = getFeatureDistance(b, [coord.lon, coord.lat]); const distB = getFeatureDistance(b, [coord.lon, coord.lat]);
@ -44,16 +47,16 @@ const IMPLICIT_SPEEDS: Record<string, number> = {
"DE:urban": 50, "DE:urban": 50,
"DE:rural": 100, // TODO: 80 (hgv weight > 3.5t, or trailer), 60 (weight > 7.5t) "DE:rural": 100, // TODO: 80 (hgv weight > 3.5t, or trailer), 60 (weight > 7.5t)
"DE:living_street": 7, "DE:living_street": 7,
"DE:bicycle_road": 30 "DE:bicycle_road": 30,
} };
export function getSpeed(maxspeed: string): number | null { export function getSpeed(maxspeed: string): number | null {
if(!isNaN(parseInt(maxspeed))) return parseInt(maxspeed); if (!isNaN(parseInt(maxspeed))) return parseInt(maxspeed);
if(maxspeed.endsWith(" mph")) { if (maxspeed.endsWith(" mph")) {
const val = parseInt(maxspeed.replace(" mph", "")); const val = parseInt(maxspeed.replace(" mph", ""));
if(!isNaN(val)) return Math.round(val * 1.60934); // Convert to km/h if (!isNaN(val)) return Math.round(val * 1.60934); // Convert to km/h
} }
if(maxspeed === "walk") return 7; // https://wiki.openstreetmap.org/wiki/Proposed_features/maxspeed_walk if (maxspeed === "walk") return 7; // https://wiki.openstreetmap.org/wiki/Proposed_features/maxspeed_walk
return IMPLICIT_SPEEDS[maxspeed as keyof typeof IMPLICIT_SPEEDS] || null; return IMPLICIT_SPEEDS[maxspeed as keyof typeof IMPLICIT_SPEEDS] || null;
} }
@ -64,11 +67,11 @@ function coordToTile(coord: WorldLocation, zoom: number) {
((1 - ((1 -
Math.log( Math.log(
Math.tan((coord.lat * Math.PI) / 180) + Math.tan((coord.lat * Math.PI) / 180) +
1 / Math.cos((coord.lat * Math.PI) / 180) 1 / Math.cos((coord.lat * Math.PI) / 180),
) / ) /
Math.PI) / Math.PI) /
2) * 2) *
Math.pow(2, z) Math.pow(2, z),
); );
return { z, x, y }; return { z, x, y };
} }
@ -117,7 +120,7 @@ export async function fetchTile(z: number, x: number, y: number) {
const pmtiles = (await hasPMTiles("tiles")) const pmtiles = (await hasPMTiles("tiles"))
? new PMTiles(new FSSource("tiles")) ? new PMTiles(new FSSource("tiles"))
: new PMTiles("https://trafficcue-tiles.picoscratch.de/germany.pmtiles"); : new PMTiles("https://trafficcue-tiles.picoscratch.de/germany.pmtiles");
const tile = (await pmtiles.getZxy(z, x, y)); const tile = await pmtiles.getZxy(z, x, y);
if (!tile) { if (!tile) {
console.log(tile); console.log(tile);
throw new Error(`Tile not found: z${z} x${x} y${y}`); throw new Error(`Tile not found: z${z} x${x} y${y}`);

View File

@ -4,15 +4,16 @@ export const eventTarget = new EventTarget();
export function localStore<T>(key: string, defaultValue: T): WrappedValue<T> { export function localStore<T>(key: string, defaultValue: T): WrappedValue<T> {
const storedValue = localStorage.getItem(key); const storedValue = localStorage.getItem(key);
const initialValue = storedValue !== null ? JSON.parse(storedValue) : defaultValue; const initialValue =
storedValue !== null ? JSON.parse(storedValue) : defaultValue;
const state = $state<WrappedValue<T>>({ current: initialValue }); const state = $state<WrappedValue<T>>({ current: initialValue });
eventTarget.addEventListener("localStorageChanged", (event) => { eventTarget.addEventListener("localStorageChanged", (event) => {
const customEvent = event as CustomEvent; const customEvent = event as CustomEvent;
console.log("localStorageChanged event received", customEvent.detail); console.log("localStorageChanged event received", customEvent.detail);
if(customEvent.detail.key === key) { if (customEvent.detail.key === key) {
const newValue = customEvent.detail.value as T; const newValue = customEvent.detail.value as T;
console.log(`localStore: ${key} updated from another tab`, newValue); console.log(`localStore: ${key} updated from another tab`, newValue);
if(JSON.stringify(state.current) === JSON.stringify(newValue)) return; if (JSON.stringify(state.current) === JSON.stringify(newValue)) return;
state.current = newValue; state.current = newValue;
} }
}); });
@ -22,8 +23,12 @@ export function localStore<T>(key: string, defaultValue: T): WrappedValue<T> {
}, },
set current(newValue: T) { set current(newValue: T) {
state.current = newValue; state.current = newValue;
eventTarget.dispatchEvent(new CustomEvent("localStorageChanged", { detail: { key, value: newValue } })); eventTarget.dispatchEvent(
new CustomEvent("localStorageChanged", {
detail: { key, value: newValue },
}),
);
localStorage.setItem(key, JSON.stringify(newValue)); localStorage.setItem(key, JSON.stringify(newValue));
}, },
}; };
} }