style: run prettier
This commit is contained in:
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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]),
|
||||||
|
|||||||
@ -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"
|
||||||
|
/>
|
||||||
|
|||||||
@ -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;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}`);
|
||||||
|
|||||||
@ -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));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user