style: run eslint and prettier
All checks were successful
TrafficCue CI / check (push) Successful in 58s
TrafficCue CI / build (push) Successful in 47s

This commit is contained in:
Cfp
2025-08-11 18:59:04 +02:00
parent 9d5319aee2
commit d7db55cd29
19 changed files with 1006 additions and 936 deletions

View File

@ -1,2 +1,3 @@
android/ android/
dist/ dist/
src-tauri/

View File

@ -26,7 +26,7 @@ export default tseslint.config(
], ],
}, },
}, },
[globalIgnores(["./android", "./dist"])], [globalIgnores(["./android", "./dist", "./src-tauri"])],
{ {
languageOptions: { languageOptions: {
globals: { globals: {

View File

@ -3,7 +3,10 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />
<title>TrafficCue</title> <title>TrafficCue</title>
</head> </head>

View File

@ -1,7 +1,6 @@
{ {
"version": 8, "version": 8,
"sources": { "sources": {},
},
"sprite": "https://tiles.openfreemap.org/sprites/ofm_f384/ofm", "sprite": "https://tiles.openfreemap.org/sprites/ofm_f384/ofm",
"glyphs": "https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf", "glyphs": "https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf",
"layers": [] "layers": []

View File

@ -10,7 +10,11 @@
import { location } from "./location.svelte"; import { location } from "./location.svelte";
import { saved } from "$lib/saved.svelte"; import { saved } from "$lib/saved.svelte";
import RoutingLayers from "$lib/services/navigation/RoutingLayers.svelte"; import RoutingLayers from "$lib/services/navigation/RoutingLayers.svelte";
import { getPMTilesURL, hasPMTiles, protocol } from "$lib/services/OfflineTiles"; import {
getPMTilesURL,
hasPMTiles,
protocol,
} from "$lib/services/OfflineTiles";
import { layers, worldLayers } from "$lib/mapLayers"; import { layers, worldLayers } from "$lib/mapLayers";
import { PMTilesProtocol } from "svelte-maplibre-gl/pmtiles"; import { PMTilesProtocol } from "svelte-maplibre-gl/pmtiles";
@ -44,31 +48,25 @@
// @ts-expect-error - not typed // @ts-expect-error - not typed
window.map = map.value; window.map = map.value;
// const worldUrl = await getPMTiles("world"); map.value!.addSource("ne2_shaded", {
// if(worldUrl) { // TODO: rename to world
map.value!.addSource("ne2_shaded", { // TODO: rename to world
type: "vector", type: "vector",
url: await getPMTilesURL("world"), url: await getPMTilesURL("world"),
attribution: "Natural Earth", attribution: "Natural Earth",
// maxzoom: 6 });
})
// @ts-expect-error - not typed correctly // @ts-expect-error - not typed correctly
worldLayers.forEach(l => map.value!.addLayer(l)); worldLayers.forEach((l) => map.value!.addLayer(l));
// }
// const url = await getPMTiles("tiles");
// console.log(url)
// if(url) {
map.value!.addSource("openmaptiles", { map.value!.addSource("openmaptiles", {
type: "vector", type: "vector",
url: await hasPMTiles("tiles") ? await getPMTilesURL("tiles") : "pmtiles://https://trafficcue-tiles.picoscratch.de/germany.pmtiles" url: (await hasPMTiles("tiles"))
}) ? await getPMTilesURL("tiles")
: "pmtiles://https://trafficcue-tiles.picoscratch.de/germany.pmtiles",
});
// @ts-expect-error - not typed correctly // @ts-expect-error - not typed correctly
layers.forEach(l => map.value!.addLayer(l)); layers.forEach((l) => map.value!.addLayer(l));
// }
}} }}
onclick={(e) => { onclick={(e) => {
if (view.current.type == "main" || view.current.type == "info") { if (view.current.type == "main" || view.current.type == "info") {

View File

@ -27,8 +27,6 @@
import * as Popover from "../ui/popover"; import * as Popover from "../ui/popover";
import { routing } from "$lib/services/navigation/routing.svelte"; import { routing } from "$lib/services/navigation/routing.svelte";
import InRouteSidebar from "./sidebar/InRouteSidebar.svelte"; import InRouteSidebar from "./sidebar/InRouteSidebar.svelte";
import say from "$lib/services/navigation/TTS";
import { downloadPMTiles } from "$lib/services/OfflineTiles";
import SettingsSidebar from "./sidebar/settings/SettingsSidebar.svelte"; import SettingsSidebar from "./sidebar/settings/SettingsSidebar.svelte";
import AboutSidebar from "./sidebar/settings/AboutSidebar.svelte"; import AboutSidebar from "./sidebar/settings/AboutSidebar.svelte";
import OfflineMapsSidebar from "./sidebar/settings/OfflineMapsSidebar.svelte"; import OfflineMapsSidebar from "./sidebar/settings/OfflineMapsSidebar.svelte";
@ -45,7 +43,7 @@
settings: SettingsSidebar, settings: SettingsSidebar,
about: AboutSidebar, about: AboutSidebar,
"offline-maps": OfflineMapsSidebar, "offline-maps": OfflineMapsSidebar,
"dev-options": DeveloperSidebar "dev-options": DeveloperSidebar,
}; };
let isDragging = false; let isDragging = false;
@ -164,9 +162,11 @@
<UserIcon /> <UserIcon />
</button> </button>
</RequiresCapability> </RequiresCapability>
<button onclick={() => { <button
onclick={() => {
view.switch("settings"); view.switch("settings");
}}> }}
>
<SettingsIcon /> <SettingsIcon />
</button> </button>
<!-- <button onclick={() => { <!-- <button onclick={() => {

View File

@ -6,18 +6,21 @@
const dev = getDeveloperToggle(); const dev = getDeveloperToggle();
</script> </script>
<SidebarHeader> <SidebarHeader>About</SidebarHeader>
About
</SidebarHeader>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions --> <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<h1 style="font-size: 2em; font-weight: bold;" onclick={() => { <h1
style="font-size: 2em; font-weight: bold;"
onclick={() => {
count--; count--;
if (count == 0) { if (count == 0) {
dev.current = "true"; dev.current = "true";
} }
}}>TrafficCue</h1> }}
>
TrafficCue
</h1>
<span>Powered by:</span> <span>Powered by:</span>
<ul> <ul>
<li>© OpenStreetMap contributors</li> <li>© OpenStreetMap contributors</li>

View File

@ -10,31 +10,42 @@
const dev = getDeveloperToggle(); const dev = getDeveloperToggle();
</script> </script>
<SidebarHeader> <SidebarHeader>Developer Settings</SidebarHeader>
Developer Settings
</SidebarHeader>
<div id="sections"> <div id="sections">
<section> <section>
<h2>Test</h2> <h2>Test</h2>
<SettingsButton icon={SpeechIcon} text="Test TTS" onclick={async () => { <SettingsButton
await say("Test") icon={SpeechIcon}
}} /> text="Test TTS"
<SettingsButton icon={MapIcon} disabled={!window.__TAURI__} text="Download tiles from URL{window.__TAURI__ ? "" : " (Unavailable)"}" onclick={async () => { onclick={async () => {
await say("Test");
}}
/>
<SettingsButton
icon={MapIcon}
disabled={!window.__TAURI__}
text="Download tiles from URL{window.__TAURI__ ? '' : ' (Unavailable)'}"
onclick={async () => {
const name = prompt("Name?"); const name = prompt("Name?");
if (!name) return; if (!name) return;
const url = prompt("URL?"); const url = prompt("URL?");
if (!url) return; if (!url) return;
await downloadPMTiles(url, name); await downloadPMTiles(url, name);
}} /> }}
/>
</section> </section>
<section> <section>
<h2>Other</h2> <h2>Other</h2>
<SettingsButton icon={ToggleLeftIcon} text="Disable Developer Options" onclick={async () => { <SettingsButton
icon={ToggleLeftIcon}
text="Disable Developer Options"
onclick={async () => {
dev.current = "false"; dev.current = "false";
view.back(); view.back();
}} /> }}
/>
</section> </section>
</div> </div>

View File

@ -4,12 +4,10 @@
import SettingsButton from "./SettingsButton.svelte"; import SettingsButton from "./SettingsButton.svelte";
import SidebarHeader from "../SidebarHeader.svelte"; import SidebarHeader from "../SidebarHeader.svelte";
let progresses: {[key: string]: number} = $state({}); let progresses: Record<string, number> = $state({});
</script> </script>
<SidebarHeader> <SidebarHeader>Offline Maps</SidebarHeader>
Offline Maps
</SidebarHeader>
{#await getRemoteList()} {#await getRemoteList()}
<p>Loading...</p> <p>Loading...</p>
@ -24,13 +22,23 @@
{/if} {/if}
{#each list as item, _index (item.file)} {#each list as item, _index (item.file)}
<SettingsButton disabled={!window.__TAURI__} icon={DownloadCloudIcon} text={item.name} progress={progresses[item.file] ?? -1} onclick={async () => { <SettingsButton
await downloadPMTiles("https://trafficcue-tiles.picoscratch.de/" + item.file, item.name.includes("World") ? "world": "tiles", (progress, total) => { disabled={!window.__TAURI__}
icon={DownloadCloudIcon}
text={item.name}
progress={progresses[item.file] ?? -1}
onclick={async () => {
await downloadPMTiles(
"https://trafficcue-tiles.picoscratch.de/" + item.file,
item.name.includes("World") ? "world" : "tiles",
(progress, total) => {
progresses[item.file] = (progress / total) * 100; progresses[item.file] = (progress / total) * 100;
}); },
);
alert(`Downloaded ${item.name}`); alert(`Downloaded ${item.name}`);
location.reload(); location.reload();
}} /> }}
/>
{/each} {/each}
</div> </div>
{/await} {/await}

View File

@ -6,16 +6,31 @@
import Progress from "$lib/components/ui/progress/progress.svelte"; import Progress from "$lib/components/ui/progress/progress.svelte";
const { const {
icon: Icon, text, view: viewName, onclick, disabled, progress icon: Icon,
text,
view: viewName,
onclick,
disabled,
progress,
}: { }: {
icon: Component<IconProps>, text: string, view?: string, onclick?: () => void, disabled?: boolean, progress?: number icon: Component<IconProps>;
text: string;
view?: string;
onclick?: () => void;
disabled?: boolean;
progress?: number;
} = $props(); } = $props();
</script> </script>
<Button variant="secondary" style="width: 100%; height: 40px;" {disabled} onclick={() => { <Button
if(viewName) view.switch(viewName) variant="secondary"
style="width: 100%; height: 40px;"
{disabled}
onclick={() => {
if (viewName) view.switch(viewName);
if (onclick) onclick(); if (onclick) onclick();
}}> }}
>
<Icon /> <Icon />
{text} {text}
{#if progress == -1} {#if progress == -1}

View File

@ -1,5 +1,11 @@
<script> <script>
import { CodeIcon, InfoIcon, LanguagesIcon, MapIcon, PaintbrushIcon } from "@lucide/svelte"; import {
CodeIcon,
InfoIcon,
LanguagesIcon,
MapIcon,
PaintbrushIcon,
} from "@lucide/svelte";
import SidebarHeader from "../SidebarHeader.svelte"; import SidebarHeader from "../SidebarHeader.svelte";
import SettingsButton from "./SettingsButton.svelte"; import SettingsButton from "./SettingsButton.svelte";
import { getDeveloperToggle } from "./developer.svelte"; import { getDeveloperToggle } from "./developer.svelte";
@ -7,9 +13,7 @@
const dev = getDeveloperToggle(); const dev = getDeveloperToggle();
</script> </script>
<SidebarHeader> <SidebarHeader>Settings</SidebarHeader>
Settings
</SidebarHeader>
<div id="sections"> <div id="sections">
<section> <section>
@ -26,7 +30,11 @@
<section> <section>
<h2>About</h2> <h2>About</h2>
{#if dev.current == "true"} {#if dev.current == "true"}
<SettingsButton icon={CodeIcon} text="Developer Settings" view="dev-options" /> <SettingsButton
icon={CodeIcon}
text="Developer Settings"
view="dev-options"
/>
{/if} {/if}
<SettingsButton icon={InfoIcon} text="About" view="about" /> <SettingsButton icon={InfoIcon} text="About" view="about" />
</section> </section>

View File

@ -1,7 +1,7 @@
export function getDeveloperToggle() { export function getDeveloperToggle() {
const value = localStorage.getItem("developer") const value = localStorage.getItem("developer");
const state = $state({ const state = $state({
current: value == null ? "false" : value current: value == null ? "false" : value,
}); });
$effect(() => { $effect(() => {

View File

@ -14,7 +14,10 @@
<ProgressPrimitive.Root <ProgressPrimitive.Root
bind:ref bind:ref
data-slot="progress" data-slot="progress"
class={cn("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", className)} class={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className,
)}
{value} {value}
{max} {max}
{...restProps} {...restProps}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,20 @@
import { appDataDir, join } from "@tauri-apps/api/path"; import { appDataDir, join } from "@tauri-apps/api/path";
import { BaseDirectory, exists, mkdir, open, remove, SeekMode } from "@tauri-apps/plugin-fs"; import {
BaseDirectory,
exists,
mkdir,
open,
remove,
SeekMode,
} from "@tauri-apps/plugin-fs";
import { download } from "@tauri-apps/plugin-upload"; import { download } from "@tauri-apps/plugin-upload";
import { PMTiles, TileType, type Source } from "pmtiles"; import { PMTiles, TileType, type Source } from "pmtiles";
export async function downloadPMTiles(url: string, name: string, onprogress?: (progress: number, total: number) => any): Promise<void> { export async function downloadPMTiles(
url: string,
name: string,
onprogress?: (progress: number, total: number) => void,
): Promise<void> {
// if(!window.__TAURI__) { // if(!window.__TAURI__) {
// throw new Error("Tauri environment is not available."); // throw new Error("Tauri environment is not available.");
// } // }
@ -12,7 +23,7 @@ export async function downloadPMTiles(url: string, name: string, onprogress?: (p
const baseDir = BaseDirectory.AppData; const baseDir = BaseDirectory.AppData;
const appDataDirPath = await appDataDir(); const appDataDirPath = await appDataDir();
if(!await exists(appDataDirPath)) { if (!(await exists(appDataDirPath))) {
await mkdir(appDataDirPath, { recursive: true }); await mkdir(appDataDirPath, { recursive: true });
} }
@ -33,15 +44,21 @@ export async function downloadPMTiles(url: string, name: string, onprogress?: (p
let totalProgress = 0; let totalProgress = 0;
await download(url, path, ({ progress, total }) => { await download(url, path, ({ progress, total }) => {
totalProgress += progress; totalProgress += progress;
console.log(`Download progress: ${Math.round((totalProgress / total) * 100)}% (${totalProgress}\tof ${total} bytes)`); console.log(
`Download progress: ${Math.round((totalProgress / total) * 100)}% (${totalProgress}\tof ${total} bytes)`,
);
if (onprogress) onprogress(totalProgress, total); if (onprogress) onprogress(totalProgress, total);
}); });
console.log(`Download completed: ${path}`); console.log(`Download completed: ${path}`);
} }
export async function getRemoteList(): Promise<{ name: string, file: string }[]> { export async function getRemoteList(): Promise<
return await fetch("https://trafficcue-tiles.picoscratch.de/index.json").then(res => res.json()); { name: string; file: string }[]
> {
return await fetch("https://trafficcue-tiles.picoscratch.de/index.json").then(
(res) => res.json(),
);
} }
export async function hasPMTiles(name: string): Promise<boolean> { export async function hasPMTiles(name: string): Promise<boolean> {
@ -53,7 +70,7 @@ export async function hasPMTiles(name: string): Promise<boolean> {
const baseDir = BaseDirectory.AppData; const baseDir = BaseDirectory.AppData;
const appDataDirPath = await appDataDir(); const appDataDirPath = await appDataDir();
if(!await exists(appDataDirPath)) { if (!(await exists(appDataDirPath))) {
return false; return false;
} }
@ -69,14 +86,14 @@ export async function getPMTilesURL(name: string) {
const baseDir = BaseDirectory.AppData; const baseDir = BaseDirectory.AppData;
const appDataDirPath = await appDataDir(); const appDataDirPath = await appDataDir();
if(!await exists(appDataDirPath)) { if (!(await exists(appDataDirPath))) {
return `pmtiles://https://trafficcue-tiles.picoscratch.de/${name}.pmtiles`; return `pmtiles://https://trafficcue-tiles.picoscratch.de/${name}.pmtiles`;
// throw new Error("App data directory does not exist."); // throw new Error("App data directory does not exist.");
} }
const filePath = await join(appDataDirPath, filename); const filePath = await join(appDataDirPath, filename);
if(!await exists(filePath, { baseDir })) { if (!(await exists(filePath, { baseDir }))) {
return `pmtiles://https://trafficcue-tiles.picoscratch.de/${name}.pmtiles`; return `pmtiles://https://trafficcue-tiles.picoscratch.de/${name}.pmtiles`;
// throw new Error(`PMTiles file not found: ${filePath}`); // throw new Error(`PMTiles file not found: ${filePath}`);
} }
@ -84,8 +101,15 @@ export async function getPMTilesURL(name: string) {
return `tiles://${name}`; return `tiles://${name}`;
} }
async function readBytes(name: string, offset: number, length: number): Promise<Uint8Array> { async function readBytes(
const file = await open(name + ".pmtiles", { read: true, baseDir: BaseDirectory.AppData }); name: string,
offset: number,
length: number,
): Promise<Uint8Array> {
const file = await open(name + ".pmtiles", {
read: true,
baseDir: BaseDirectory.AppData,
});
const buffer = new Uint8Array(length); const buffer = new Uint8Array(length);
await file.seek(offset, SeekMode.Start); await file.seek(offset, SeekMode.Start);
await file.read(buffer); await file.read(buffer);
@ -100,14 +124,20 @@ export class FSSource implements Source {
this.name = name; this.name = name;
} }
async getBytes(offset: number, length: number, _signal?: AbortSignal, _etag?: string) { // TODO: abort signal async getBytes(
offset: number,
length: number,
_signal?: AbortSignal,
_etag?: string,
) {
// TODO: abort signal
const data = await readBytes(this.name, offset, length); const data = await readBytes(this.name, offset, length);
return { return {
data: data.buffer as ArrayBuffer, data: data.buffer as ArrayBuffer,
etag: undefined, etag: undefined,
cacheControl: undefined, cacheControl: undefined,
expires: undefined expires: undefined,
} };
} }
getKey = () => this.name; getKey = () => this.name;
@ -121,12 +151,13 @@ interface RequestParameters {
type?: "string" | "json" | "arrayBuffer" | "image"; type?: "string" | "json" | "arrayBuffer" | "image";
credentials?: "same-origin" | "include"; credentials?: "same-origin" | "include";
collectResourceTiming?: boolean; collectResourceTiming?: boolean;
}; }
export class Protocol { export class Protocol {
/** @hidden */ /** @hidden */
tiles: Map<string, PMTiles>; tiles: Map<string, PMTiles>;
metadata: boolean; errorOnMissingTile: boolean; metadata: boolean;
errorOnMissingTile: boolean;
/** /**
* Initialize the MapLibre PMTiles protocol. * Initialize the MapLibre PMTiles protocol.
@ -163,7 +194,7 @@ export class Protocol {
/** @hidden */ /** @hidden */
tilev4 = async ( tilev4 = async (
params: RequestParameters, params: RequestParameters,
abortController: AbortController abortController: AbortController,
) => { ) => {
if (params.type === "json") { if (params.type === "json") {
const pmtilesUrl = params.url.substr(8); const pmtilesUrl = params.url.substr(8);
@ -183,7 +214,7 @@ export class Protocol {
if (h.minLon >= h.maxLon || h.minLat >= h.maxLat) { if (h.minLon >= h.maxLon || h.minLat >= h.maxLat) {
console.error( console.error(
`Bounds of PMTiles archive ${h.minLon},${h.minLat},${h.maxLon},${h.maxLat} are not valid.` `Bounds of PMTiles archive ${h.minLon},${h.minLat},${h.maxLon},${h.maxLat} are not valid.`,
); );
} }

View File

@ -60,7 +60,12 @@ out center tags;`,
}).then((res) => res.json() as Promise<OverpassResult>); }).then((res) => res.json() as Promise<OverpassResult>);
} }
export async function fetchNearbyPOI(lat: number, lon: number, tags: string[], radius: number) { export async function fetchNearbyPOI(
lat: number,
lon: number,
tags: string[],
radius: number,
) {
return await fetch(OVERPASS_SERVER, { return await fetch(OVERPASS_SERVER, {
method: "POST", method: "POST",
body: `[out:json]; body: `[out:json];

View File

@ -23,7 +23,7 @@ export default async function say(text: string) {
duck(); duck();
if (tts !== "web") { if (tts !== "web") {
try { try {
await invoke("plugin:tts|speak", { text }) await invoke("plugin:tts|speak", { text });
} catch (e) { } catch (e) {
console.error("Error speaking text", e); console.error("Error speaking text", e);
alert(e); alert(e);

View File

@ -12,22 +12,23 @@ export default defineConfig({
port: 5173, port: 5173,
strictPort: true, strictPort: true,
host: host || false, host: host || false,
hmr: host ? { hmr: host
? {
protocol: "ws", protocol: "ws",
host, host,
port: 1421 port: 1421,
} : undefined,
watch: {
ignored: ["**/src-tauri/**"]
} }
: undefined,
watch: {
ignored: ["**/src-tauri/**"],
},
}, },
envPrefix: ["VITE_", "TAURI_ENV_*"], envPrefix: ["VITE_", "TAURI_ENV_*"],
build: { build: {
target: process.env.TAURI_ENV_PLATFORM == "windows" target:
? "chrome105" process.env.TAURI_ENV_PLATFORM == "windows" ? "chrome105" : "safari13",
: "safari13",
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
sourcemap: !!process.env.TAURI_ENV_DEBUG sourcemap: !!process.env.TAURI_ENV_DEBUG,
}, },
resolve: { resolve: {
alias: { alias: {