more work on print view

This commit is contained in:
Christian Beutel
2024-04-10 02:05:13 +02:00
parent 10caa8b768
commit e44ba65bd9
14 changed files with 1077 additions and 27 deletions

View File

@@ -3,13 +3,16 @@
import { currentUser } from "$lib/stores/user_store";
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
import type { Map, Marker } from "leaflet";
import { onMount } from "svelte";
import { createEventDispatcher, onMount } from "svelte";
export let trail: Trail;
export let markers: Marker[] = [];
export let map: Map | null = null;
export let options = {};
const dispatch = createEventDispatcher();
let L: any;
let map: Map;
let controlElevation: any;
$: if (trail.expand.gpx_data && controlElevation) {
@@ -23,14 +26,21 @@
//@ts-ignore
await import("$lib/vendor/leaflet-elevation/src/index.js");
map = L.map("map").setView([trail.lat ?? 0, trail.lon ?? 0], 14);
map.attributionControl.setPrefix(false)
map = L.map("map", { preferCanvas: true }).setView(
[trail.lat ?? 0, trail.lon ?? 0],
14,
);
map!.attributionControl.setPrefix(false);
map!.on("zoomend", function () {
dispatch("zoomend", map);
});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors",
}).addTo(map);
const elevation_options = {
const default_elevation_options = {
height: 200,
theme: "lightblue-theme",
@@ -84,11 +94,16 @@
},
};
const elevation_options = Object.assign(
default_elevation_options,
options,
);
controlElevation = L.control.elevation(elevation_options).addTo(map);
for (const waypoint of trail.expand.waypoints) {
const marker = createMarkerFromWaypoint(L, waypoint);
marker.addTo(map);
marker.addTo(map!);
markers.push(marker);
}
});
@@ -96,16 +111,8 @@
<div id="map-container" class="flex flex-col">
<div id="map" class="rounded-xl z-0 basis-full"></div>
<div id="elevation"></div>
<div class="flex items-center justify-between">
<slot />
<div class="basis-full min-w-[300px]" id="elevation"></div>
</div>
</div>
<style>
#map-container {
height: calc(100vh - 180px);
}
@media only screen and (min-width: 768px) {
#map-container {
height: calc(100vh - 124px);
}
}
</style>

View File

@@ -1,5 +1,5 @@
import type { Waypoint } from "$lib/models/waypoint";
import type { Icon, LeafletEvent, Marker } from "leaflet";
import type { Icon, LeafletEvent, Map, Marker } from "leaflet";
export function createMarkerFromWaypoint(L: any, waypoint: Waypoint, onDragEnd?: (event: LeafletEvent) => void): Marker {
const fontAwesomeIcon = L.AwesomeMarkers.icon({
@@ -27,4 +27,43 @@ export function createMarkerFromWaypoint(L: any, waypoint: Waypoint, onDragEnd?:
}
return marker;
}
export function calculatePixelPerMeter(map: Map, meters: number) {
const y = map.getSize().y;
const x = map.getSize().x;
const maxMeters = map.containerPointToLatLng([0, y]).distanceTo(map.containerPointToLatLng([x, y]));
const pixelPerMeter = x / maxMeters;
return pixelPerMeter * meters
}
export function calculateScaleFactor(map: Map) {
function _pxTOmm() {
let heightRef = document.createElement('div');
heightRef.style.height = '1mm';
heightRef.style.position = "absolute";
heightRef.id = 'heightRef';
document.body.appendChild(heightRef);
const pxPermm = heightRef.getBoundingClientRect().height;
document.body.removeChild(heightRef);
return function pxTOmm(px: number) {
return px / pxPermm;
}
}
var centerOfMap = map.getSize().y / 2;
var realWorlMetersPer100Pixels = map.distance(
map.containerPointToLatLng([0, centerOfMap]),
map.containerPointToLatLng([100, centerOfMap])
);
const screenMetersPer100Pixels = _pxTOmm()(100) / 1000;
const scaleFactor = realWorlMetersPer100Pixels / screenMetersPer100Pixels
return scaleFactor
}

View File

@@ -0,0 +1,29 @@
export function createRect(
x: number,
y: number,
width: number,
height: number,
fill: string,
) {
const svgns = "http://www.w3.org/2000/svg";
const rect = document.createElementNS(svgns, "rect");
rect.setAttribute("x", x.toString());
rect.setAttribute("y", y.toString());
rect.setAttribute("width", width.toString());
rect.setAttribute("height", height.toString());
rect.setAttribute("fill", fill);
return rect;
}
export function createText(content: string, x: number,
y: number, fontSize?: string) {
const svgns = "http://www.w3.org/2000/svg";
const text = document.createElementNS(svgns, "text");
text.setAttribute("x", x.toString());
text.setAttribute("y", y.toString());
text.style.fontSize = fontSize ?? ""
text.textContent = content;
return text;
}

View File

@@ -773,6 +773,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
this.track_info.name = geojson.name;
}
style.color = "#ff0000"
let layer = L.geoJson(geojson, {
distanceMarkers: distanceMarkers,
style: style,

View File

@@ -319,6 +319,17 @@
--ele-grid: rgba(var(--input-border));
}
/* GRAY THEME //////////////////////////////////////////////// */
.gray-theme {
--ele-bg: #ffffff;
--ele-area: #242734;
--ele-alpha: 0.45;
--ele-stroke: #7b7b7b;
--ele-circle: #fff;
--ele-line: #000;
--ele-grid: rgba(var(--input-border));
}
.elevation-detached.lightblue-theme .area {
stroke: #3366CC;
}

View File

@@ -8,7 +8,8 @@ export const Colors = {
'purple' : { area: '#732C7B' },
'steelblue': { area: '#4682B4' },
'red' : { area: '#F00' },
'lime' : { area: '#9CC222', line: '#566B13' }
'lime' : { area: '#9CC222', line: '#566B13' },
'gray': { area: '#000000', line: '#242734', alpha: 0.00001, stroke: '#000000' }
};
const SEC = 1000;

View File

@@ -0,0 +1,261 @@
import queue from "./queue";
var cacheBusterDate = +new Date();
// leaflet-image
export default function leafletImage(map, callback) {
var dimensions = map.getSize(),
layerQueue = new queue(1);
var canvas = document.createElement('canvas');
canvas.width = dimensions.x;
canvas.height = dimensions.y;
var ctx = canvas.getContext('2d');
// dummy canvas image when loadTile get 404 error
// and layer don't have errorTileUrl
var dummycanvas = document.createElement('canvas');
dummycanvas.width = 1;
dummycanvas.height = 1;
var dummyctx = dummycanvas.getContext('2d');
dummyctx.fillStyle = 'rgba(0,0,0,0)';
dummyctx.fillRect(0, 0, 1, 1);
// layers are drawn in the same order as they are composed in the DOM:
// tiles, paths, and then markers
map.eachLayer(drawTileLayer);
map.eachLayer(drawEsriDynamicLayer);
if (map._pathRoot) {
layerQueue.defer(handlePathRoot, map._pathRoot);
} else if (map._panes) {
var firstCanvas = map._panes.overlayPane.getElementsByTagName('canvas').item(0);
if (firstCanvas) {
layerQueue.defer(handlePathRoot, firstCanvas);
}
}
map.eachLayer(drawMarkerLayer);
layerQueue.awaitAll(layersDone);
function drawTileLayer(l) {
if (l instanceof L.TileLayer) layerQueue.defer(handleTileLayer, l);
else if (l._heat) layerQueue.defer(handlePathRoot, l._canvas);
}
function drawMarkerLayer(l) {
if (l instanceof L.Marker && l.options.icon instanceof L.Icon) {
layerQueue.defer(handleMarkerLayer, l);
}
}
function drawEsriDynamicLayer(l) {
if (!L.esri) return;
if (l instanceof L.esri.DynamicMapLayer) {
layerQueue.defer(handleEsriDymamicLayer, l);
}
}
function done() {
callback(null, canvas);
}
function layersDone(err, layers) {
if (err) throw err;
layers.forEach(function (layer) {
if (layer && layer.canvas) {
ctx.drawImage(layer.canvas, 0, 0);
}
});
done();
}
function handleTileLayer(layer, callback) {
// `L.TileLayer.Canvas` was removed in leaflet 1.0
var isCanvasLayer = (L.TileLayer.Canvas && layer instanceof L.TileLayer.Canvas),
canvas = document.createElement('canvas');
canvas.width = dimensions.x;
canvas.height = dimensions.y;
var ctx = canvas.getContext('2d'),
bounds = map.getPixelBounds(),
zoom = map.getZoom(),
tileSize = layer.options.tileSize;
if (zoom > layer.options.maxZoom ||
zoom < layer.options.minZoom
) {
return callback();
}
var tileBounds = L.bounds(
bounds.min.divideBy(tileSize)._floor(),
bounds.max.divideBy(tileSize)._floor()),
tiles = [],
j, i,
tileQueue = new queue(1);
for (j = tileBounds.min.y; j <= tileBounds.max.y; j++) {
for (i = tileBounds.min.x; i <= tileBounds.max.x; i++) {
tiles.push(new L.Point(i, j));
}
}
tiles.forEach(function (tilePoint) {
var originalTilePoint = tilePoint.clone();
if (layer._adjustTilePoint) {
layer._adjustTilePoint(tilePoint);
}
var tilePos = originalTilePoint
.scaleBy(new L.Point(tileSize, tileSize))
.subtract(bounds.min);
if (tilePoint.y >= 0) {
if (isCanvasLayer) {
var tile = layer._tiles[tilePoint.x + ':' + tilePoint.y];
tileQueue.defer(canvasTile, tile, tilePos, tileSize);
} else {
var url = addCacheString(layer.getTileUrl(tilePoint));
tileQueue.defer(loadTile, url, tilePos, tileSize);
}
}
});
tileQueue.awaitAll(tileQueueFinish);
function canvasTile(tile, tilePos, tileSize, callback) {
callback(null, {
img: tile,
pos: tilePos,
size: tileSize
});
}
function loadTile(url, tilePos, tileSize, callback) {
var im = new Image();
im.crossOrigin = '';
im.onload = function () {
callback(null, {
img: this,
pos: tilePos,
size: tileSize
});
};
im.onerror = function (e) {
// use canvas instead of errorTileUrl if errorTileUrl get 404
if (layer.options.errorTileUrl != '' && e.target.errorCheck === undefined) {
e.target.errorCheck = true;
e.target.src = layer.options.errorTileUrl;
} else {
callback(null, {
img: dummycanvas,
pos: tilePos,
size: tileSize
});
}
};
im.src = url;
}
function tileQueueFinish(err, data) {
data.forEach(drawTile);
callback(null, { canvas: canvas });
}
function drawTile(d) {
ctx.drawImage(d.img, Math.floor(d.pos.x), Math.floor(d.pos.y),
d.size, d.size);
}
}
function handlePathRoot(root, callback) {
var bounds = map.getPixelBounds(),
origin = map.getPixelOrigin(),
canvas = document.createElement('canvas');
canvas.width = dimensions.x;
canvas.height = dimensions.y;
var ctx = canvas.getContext('2d');
var pos = L.DomUtil.getPosition(root).subtract(bounds.min).add(origin);
try {
ctx.drawImage(root, pos.x, pos.y, canvas.width - (pos.x * 2), canvas.height - (pos.y * 2));
callback(null, {
canvas: canvas
});
} catch (e) {
console.error('Element could not be drawn on canvas', root); // eslint-disable-line no-console
}
}
function handleMarkerLayer(marker, callback) {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
pixelBounds = map.getPixelBounds(),
minPoint = new L.Point(pixelBounds.min.x, pixelBounds.min.y),
pixelPoint = map.project(marker.getLatLng()),
url = "/vendor/leaflet-image/marker.png",
icon = getComputedStyle(marker._icon.children[0], ':before').getPropertyValue('content').substring(1, 2) ?? "\uf111",
im = new Image(),
options = marker.options.icon.options,
size = options.iconSize,
pos = pixelPoint.subtract(minPoint),
anchor = L.point(options.iconAnchor || size && size.divideBy(2, true));
if (size instanceof L.Point) size = [size.x, size.y];
var x = Math.round(pos.x - size[0] + anchor.x),
y = Math.round(pos.y - anchor.y);
canvas.width = dimensions.x;
canvas.height = dimensions.y;
im.crossOrigin = '';
im.onload = function () {
ctx.drawImage(this, x, y, size[0], size[1]);
ctx.font = '600 14px "Font Awesome 6 Free"';
ctx.fillStyle = "#ffffff";
ctx.fillText(icon, x + 10.2, y + 24);
callback(null, {
canvas: canvas
});
};
im.src = url;
}
function handleEsriDymamicLayer(dynamicLayer, callback) {
var canvas = document.createElement('canvas');
canvas.width = dimensions.x;
canvas.height = dimensions.y;
var ctx = canvas.getContext('2d');
var im = new Image();
im.crossOrigin = '';
im.src = addCacheString(dynamicLayer._currentImage._image.src);
im.onload = function () {
ctx.drawImage(im, 0, 0);
callback(null, {
canvas: canvas
});
};
}
function addCacheString(url) {
// If it's a data URL we don't want to touch this.
if (isDataURL(url)) {
return url;
}
return url + ((url.match(/\?/)) ? '&' : '?') + 'cache=' + cacheBusterDate;
}
function isDataURL(url) {
var dataURLRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i;
return !!url.match(dataURLRegex);
}
};

View File

@@ -0,0 +1,120 @@
var noabort = {};
const slice = [].slice;
function Queue(size) {
this._size = size;
this._call =
this._error = null;
this._tasks = [];
this._data = [];
this._waiting =
this._active =
this._ended =
this._start = 0; // inside a synchronous task callback?
}
Queue.prototype = queue.prototype = {
constructor: Queue,
defer: function(callback) {
if (typeof callback !== "function") throw new Error("invalid callback");
if (this._call) throw new Error("defer after await");
if (this._error != null) return this;
var t = slice.call(arguments, 1);
t.push(callback);
++this._waiting, this._tasks.push(t);
poke(this);
return this;
},
abort: function() {
if (this._error == null) abort(this, new Error("abort"));
return this;
},
await: function(callback) {
if (typeof callback !== "function") throw new Error("invalid callback");
if (this._call) throw new Error("multiple await");
this._call = function(error, results) { callback.apply(null, [error].concat(results)); };
maybeNotify(this);
return this;
},
awaitAll: function(callback) {
if (typeof callback !== "function") throw new Error("invalid callback");
if (this._call) throw new Error("multiple await");
this._call = callback;
maybeNotify(this);
return this;
}
};
function poke(q) {
if (!q._start) {
try { start(q); } // let the current task complete
catch (e) {
if (q._tasks[q._ended + q._active - 1]) abort(q, e); // task errored synchronously
else if (!q._data) throw e; // await callback errored synchronously
}
}
}
function start(q) {
while (q._start = q._waiting && q._active < q._size) {
var i = q._ended + q._active,
t = q._tasks[i],
j = t.length - 1,
c = t[j];
t[j] = end(q, i);
--q._waiting, ++q._active;
t = c.apply(null, t);
if (!q._tasks[i]) continue; // task finished synchronously
q._tasks[i] = t || noabort;
}
}
function end(q, i) {
return function(e, r) {
if (!q._tasks[i]) return; // ignore multiple callbacks
--q._active, ++q._ended;
q._tasks[i] = null;
if (q._error != null) return; // ignore secondary errors
if (e != null) {
abort(q, e);
} else {
q._data[i] = r;
if (q._waiting) poke(q);
else maybeNotify(q);
}
};
}
function abort(q, e) {
var i = q._tasks.length, t;
q._error = e; // ignore active callbacks
q._data = undefined; // allow gc
q._waiting = NaN; // prevent starting
while (--i >= 0) {
if (t = q._tasks[i]) {
q._tasks[i] = null;
if (t.abort) {
try { t.abort(); }
catch (e) { /* ignore */ }
}
}
}
q._active = NaN; // allow notification
maybeNotify(q);
}
function maybeNotify(q) {
if (!q._active && q._call) {
var d = q._data;
q._data = undefined; // allow gc
q._call(q._error, d);
}
}
export default function queue(concurrency) {
if (concurrency == null) concurrency = Infinity;
else if (!((concurrency = +concurrency) >= 1)) throw new Error("invalid concurrency");
return new Queue(concurrency);
}

View File

@@ -16,9 +16,7 @@
<title>{$trail.name} | {$_("map")} | wanderer</title>
</svelte:head>
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
<div
id="trail-details"
>
<div id="trail-details">
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
</div>
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>

View File

@@ -0,0 +1,262 @@
<script lang="ts">
import { page } from "$app/stores";
import Button from "$lib/components/base/button.svelte";
import Select from "$lib/components/base/select.svelte";
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
import { trail } from "$lib/stores/trail_store";
import {
calculatePixelPerMeter,
calculateScaleFactor,
} from "$lib/util/leaflet_util";
import { createRect, createText } from "$lib/util/svg_util";
import "$lib/vendor/leaflet-elevation/src/index.css";
import leafletImage from "$lib/vendor/leaflet-image/leaflet-image.js";
import type { Map } from "leaflet";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css";
import QrCodeWithLogo from "qrcode-with-logos";
import { onMount, tick } from "svelte";
import { currentUser } from "$lib/stores/user_store";
import {
formatDistance,
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
import LogoTextLight from "$lib/components/logo/logo_text_light.svelte";
import LogoText from "$lib/components/logo/logo_text.svelte";
let map: Map;
const paperSizes: { text: string; value: keyof typeof paperDimensions }[] =
[
{ text: "A4", value: "a4" },
{ text: "Letter", value: "letter" },
];
const paperDimensions = {
a4: {
width: "21cm",
height: "29.7cm",
},
letter: {
width: "21.59cm",
height: "27.94cm",
},
};
let selectedPaperSize = paperSizes[0].value;
const orientations: { text: string; value: "portrait" | "landscape" }[] = [
{ text: "Portrait", value: "portrait" },
{ text: "Landscape", value: "landscape" },
];
let selectedOrientation: "portrait" | "landscape" = orientations[0].value;
let scale = 1;
onMount(() => {
let qrcode = new QrCodeWithLogo({
content: $page.url.href.replace("/print", ""),
image: document.getElementById("qrcode") as HTMLImageElement,
logo: {
src: "/favicon.png",
},
});
});
function print() {
leafletImage(map, function (err: string, canvas: HTMLCanvasElement) {
var img = document.createElement("img");
var dimensions = map.getSize();
img.width = dimensions.x;
img.height = dimensions.y;
img.src = canvas.toDataURL();
document.getElementById("images")!.innerHTML = "";
document.getElementById("images")!.appendChild(img);
});
}
async function updateMapSize() {
if (!map) {
return;
}
await tick();
map.invalidateSize();
updateScale(map);
}
async function updateScale(map: Map) {
const svg = document.getElementById("ruler") as HTMLElement;
if (!svg) {
return;
}
const svgRect = svg.getBoundingClientRect();
const height = svgRect.height;
const width = svgRect.width;
svg.replaceChildren();
let oneUnitInPixels = calculatePixelPerMeter(
map,
$currentUser && $currentUser.unit == "imperial" ? 1609.34 : 1000,
);
let unitsInRuler = width / oneUnitInPixels;
function createRulerSegment(
x: number,
width: number,
label: string,
fill: string,
) {
const bar = createRect(x, height / 2, width - 4, 6, fill);
const tick = createRect(x + width - 4, height / 2, 4, 6, "#000");
const text = createText(
label,
x + width - 5 * label.length,
height / 2 + 20,
"0.75rem",
);
svg.appendChild(bar);
svg.appendChild(tick);
svg.append(text);
}
let multiplier = 1;
if (unitsInRuler > 500) {
multiplier = 250;
} else if (unitsInRuler > 150) {
multiplier = 50;
} else if (unitsInRuler > 25) {
multiplier = 10;
} else if (unitsInRuler > 15) {
multiplier = 5;
} else if (unitsInRuler > 5) {
multiplier = 2;
} else if (unitsInRuler > 1.5) {
multiplier = 1;
} else if (unitsInRuler > 1) {
multiplier = 0.5;
} else if (unitsInRuler > 0.2) {
multiplier = 0.1;
} else if (unitsInRuler > 0) {
multiplier = 0.05;
}
const segmentCount = Math.floor(unitsInRuler / multiplier);
svg.appendChild(createText("0", 0, height / 2 + 20, "0.75rem"));
for (let i = 0; i < segmentCount; i++) {
createRulerSegment(
oneUnitInPixels * i * multiplier,
oneUnitInPixels * multiplier,
`${multiplier < 1 ? ((i + 1) * multiplier).toFixed(1) : (i + 1) * multiplier}`,
i % 2 == 0 ? "#7c7c7c" : "#5d5d5d",
);
}
svg.appendChild(
createText(
$currentUser && $currentUser.unit == "imperial" ? "MI" : "KM",
segmentCount * multiplier * oneUnitInPixels - 20,
height / 2 - 7,
),
);
scale = calculateScaleFactor(map);
}
</script>
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr] gap-x-1 gap-y-4">
<div
class="print-details px-6 space-y-4 border border-input-border rounded-3xl"
>
<div id="images"></div>
<h1 class="text-4xl font-bold">Print Trail</h1>
<Select
bind:value={selectedPaperSize}
items={paperSizes}
label="Paper size"
on:change={updateMapSize}
></Select>
<Select
bind:value={selectedOrientation}
items={orientations}
label="Orientation"
on:change={updateMapSize}
></Select>
<Button primary={true} on:click={print}>Print!</Button>
</div>
<div class="paper-container overflow-scroll">
<div
class="paper flex flex-col mx-auto shadow-xl mb-8 p-8 bg-white"
style="width: {paperDimensions[selectedPaperSize][
selectedOrientation == 'portrait' ? 'width' : 'height'
]}; height: {paperDimensions[selectedPaperSize][
selectedOrientation == 'portrait' ? 'height' : 'width'
]}"
>
<div class="basis-full">
<MapWithElevation
trail={$trail}
options={{
theme: "gray-theme",
slope: false,
speed: false,
legend: false,
time: false,
hotline: false,
height: 150,
}}
on:zoomend={(e) => updateScale(e.detail)}
bind:map
>
<img
class="w-[100px] h-[100px]"
id="qrcode"
alt="QR Code"
/>
<div class="basis-full mx-4">
<svg width="100%" height="50" id="ruler"> </svg>
<span class="text-sm"
>Scale: &nbsp 1 : {Math.round(scale)}</span
>
</div>
</MapWithElevation>
</div>
<div class="pt-4 -mt-4 border-t border-t-input-border z-10">
<div class="float-right"><LogoText></LogoText></div>
<h4 class="font-semibold">{$trail.name}</h4>
<h5>
<i class="fa fa-location-dot mr-2"></i>{$trail.location ||
"-"}
</h5>
<div
class="flex mt-1 gap-4 text-sm text-gray-500 whitespace-nowrap"
>
<span
><i class="fa fa-left-right mr-2"></i>{formatDistance(
$trail.distance,
)}</span
>
<span
><i class="fa fa-up-down mr-2"></i>{formatElevation(
$trail.elevation_gain,
)}</span
>
<span
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
$trail.duration,
)}</span
>
</div>
</div>
</div>
</div>
</main>
<style>
:global(#map-container) {
height: 100%;
}
</style>

View File

@@ -0,0 +1,19 @@
import { lists_index } from "$lib/stores/list_store";
import { trails_show } from "$lib/stores/trail_store";
import { error, type Load } from "@sveltejs/kit";
import { ClientResponseError } from "pocketbase";
export const load: Load = async ({ params, fetch }) => {
try {
await trails_show(params.id!, true, fetch)
} catch (e) {
if (e instanceof ClientResponseError && e.status == 404) {
error(404, {
message: 'Not found'
});
}
console.log(e);
}
await lists_index(undefined, fetch);
};