Trail Table: Allow multi select (#264)
* trail table: multi select * trail cards: multi select * multiselect for list view, several multiselect fixes, code beautify * remove unnecessary imports, move code to simplify diff of trail_dropdown * translations * fix horizontal scrollbar in table view * direct export from list, multiselect export * fix refreshing share icon after sharing from trail list * fix adding multiple trails to a trail-list * fix retrieving mail notification template * migrates to v0.17.0 --------- Co-authored-by: Christian Beutel <>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,4 +11,4 @@ run.sh
|
||||
build*.sh
|
||||
start.*
|
||||
|
||||
data*/
|
||||
data*/
|
||||
|
||||
@@ -59,7 +59,7 @@ func GenerateHTML(appUrl string, recipientName string, authorName string, notifi
|
||||
}
|
||||
|
||||
html, err := registry.LoadFiles(
|
||||
"templates/mail/notification.html",
|
||||
"db/templates/mail/notification.html",
|
||||
).Render(content)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
try {
|
||||
const actors: Actor[] = await searchActors(q, includeSelf);
|
||||
searchItems = actors.map((a) => ({
|
||||
text: "a.username!",
|
||||
text: a.username,
|
||||
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
|
||||
value: a,
|
||||
icon: "user",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { type Snippet } from "svelte";
|
||||
|
||||
import type { List } from "$lib/models/list";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
@@ -12,11 +13,12 @@
|
||||
|
||||
interface Props {
|
||||
lists: List[];
|
||||
trails?: Set<Trail> | undefined;
|
||||
children?: Snippet<[any]>;
|
||||
onchange?: (list: List) => void
|
||||
}
|
||||
|
||||
let { lists, children, onchange }: Props = $props();
|
||||
let { lists, trails, children, onchange }: Props = $props();
|
||||
|
||||
let modal: Modal;
|
||||
|
||||
@@ -29,6 +31,20 @@
|
||||
modal.closeModal!();
|
||||
}
|
||||
|
||||
function listContainsAllTrails(list: List) : boolean {
|
||||
if (trails === undefined) {
|
||||
return listContainsCurrentTrail(list) ?? false;
|
||||
} else if (list.trails !== undefined) {
|
||||
for (const lTrail of trails) {
|
||||
if (!list.trails!.includes(lTrail.id!)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function listContainsCurrentTrail(list: List) {
|
||||
return list.trails?.includes($trail.id!);
|
||||
}
|
||||
@@ -66,7 +82,7 @@
|
||||
<h5 class="text-md font-semibold">{list.name}</h5>
|
||||
|
||||
<i
|
||||
class="fa fa-{listContainsCurrentTrail(list)
|
||||
class="fa fa-{listContainsAllTrails(list)
|
||||
? 'minus'
|
||||
: 'plus'} rounded-full border border-input-border p-2"
|
||||
></i>
|
||||
|
||||
@@ -18,15 +18,21 @@
|
||||
interface Props {
|
||||
trail: Trail;
|
||||
fullWidth?: boolean;
|
||||
selected: boolean;
|
||||
hovered: boolean;
|
||||
onmouseenter?: MouseEventHandler<HTMLDivElement>;
|
||||
onmouseleave?: MouseEventHandler<HTMLDivElement>;
|
||||
onTrailSelect?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
trail,
|
||||
fullWidth = false,
|
||||
selected = false,
|
||||
hovered = false,
|
||||
onmouseenter,
|
||||
onmouseleave,
|
||||
onTrailSelect,
|
||||
}: Props = $props();
|
||||
|
||||
let thumbnail = $derived(
|
||||
@@ -45,6 +51,11 @@
|
||||
(trail.expand?.trail_share_via_trail?.length ?? 0) > 0,
|
||||
);
|
||||
|
||||
function handleInputClick(e: Event) {
|
||||
e.stopPropagation();
|
||||
onTrailSelect?.();
|
||||
hovered = true;
|
||||
}
|
||||
// expand and collapse the tags
|
||||
let expandedTags = $state(false);
|
||||
|
||||
@@ -85,18 +96,17 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $currentUser && trail.like_count > 0}
|
||||
{#if hovered || selected}
|
||||
<div
|
||||
class="flex absolute items-center justify-center top-4 left-4 bg-background w-8 h-8 rounded-full"
|
||||
class="flex absolute top-4 left-4 w-8 h-8 rounded-full items-center justify-center bg-background text-content"
|
||||
>
|
||||
<span class="tooltip" data-title={$_("likes")}>
|
||||
<i class="fa fa-heart"></i>
|
||||
</span>
|
||||
<div
|
||||
class="absolute pointer-events-none left-5 -top-1 text-xs rounded-full bg-menu-background px-1 text-center"
|
||||
>
|
||||
{trail.like_count}
|
||||
</div>
|
||||
<input
|
||||
id="trail-selected"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||
bind:checked={selected}
|
||||
onclick={(e) => handleInputClick(e)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if (trail.public || trailIsShared) && $currentUser}
|
||||
@@ -121,6 +131,20 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $currentUser && trail.like_count > 0}
|
||||
<div
|
||||
class="flex absolute items-center justify-center {trailIsShared || trail.public ? 'top-14': 'top-4'} right-4 bg-background w-8 h-8 rounded-full"
|
||||
>
|
||||
<span class="tooltip" data-title={$_("likes")}>
|
||||
<i class="fa fa-heart"></i>
|
||||
</span>
|
||||
<div
|
||||
class="absolute pointer-events-none left-5 -top-1 text-xs rounded-full bg-menu-background px-1 text-center"
|
||||
>
|
||||
{trail.like_count}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<h4 class="font-semibold text-lg line-clamp-2">{trail.name}</h4>
|
||||
@@ -162,9 +186,9 @@
|
||||
type="button"
|
||||
>
|
||||
{#if expandedTags}
|
||||
{$_('show-less')}
|
||||
{$_("show-less")}
|
||||
{:else}
|
||||
+{trail.tags.length - 2} {$_('more')}
|
||||
+{trail.tags.length - 2} {$_("more")}
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -20,14 +20,15 @@
|
||||
import ListSelectModal from "../list/list_select_modal.svelte";
|
||||
import TrailExportModal from "./trail_export_modal.svelte";
|
||||
import TrailShareModal from "./trail_share_modal.svelte";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
|
||||
interface Props {
|
||||
trail: Trail;
|
||||
handle: string;
|
||||
mode: "overview" | "map" | "list";
|
||||
trails?: Set<Trail> | undefined;
|
||||
mode: "overview" | "map" | "list" | "multi-select";
|
||||
onconfirm?: (resetSelection?: boolean) => void;
|
||||
}
|
||||
|
||||
let { trail, handle, mode }: Props = $props();
|
||||
let { trails, mode, onconfirm }: Props = $props();
|
||||
|
||||
let confirmModal: ConfirmModal;
|
||||
let listSelectModal: ListSelectModal;
|
||||
@@ -36,55 +37,145 @@
|
||||
|
||||
let lists: List[] = $state([]);
|
||||
|
||||
const isOwned: boolean = trail.author == $currentUser?.actor;
|
||||
|
||||
const allowEdit =
|
||||
isOwned ||
|
||||
trail.expand?.trail_share_via_trail?.some(
|
||||
(s) => s.permission == "edit",
|
||||
function allowEdit(): boolean {
|
||||
return (
|
||||
hasTrail() &&
|
||||
!isMultiselectMode() &&
|
||||
(trail()!.expand?.author?.id === $currentUser?.actor ||
|
||||
trail()!.expand?.trail_share_via_trail?.some(
|
||||
(s) => s.permission == "edit",
|
||||
))!
|
||||
);
|
||||
}
|
||||
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
mode == "overview"
|
||||
? { text: $_("show-on-map"), value: "show", icon: "map" }
|
||||
: {
|
||||
text: $_("show-in-overview"),
|
||||
value: "show",
|
||||
icon: "table-columns",
|
||||
},
|
||||
function dropdownItems(): DropdownItem[] {
|
||||
return [
|
||||
...(!isMultiselectMode()
|
||||
? [
|
||||
mode == "overview" || mode == "multi-select"
|
||||
? {
|
||||
text: $_("show-on-map"),
|
||||
value: "show",
|
||||
icon: "map",
|
||||
}
|
||||
: {
|
||||
text: $_("show-in-overview"),
|
||||
value: "show",
|
||||
icon: "table-columns",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isMultiselectMode()
|
||||
? [{ text: $_("directions"), value: "direction", icon: "car" }]
|
||||
: []),
|
||||
...(canExport()
|
||||
? [
|
||||
{
|
||||
text: $_("export"),
|
||||
value: "download",
|
||||
icon: "download",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isMultiselectMode()
|
||||
? [{ text: $_("print"), value: "print", icon: "print" }]
|
||||
: []),
|
||||
...(!isFromCurrentUser()
|
||||
? []
|
||||
: [
|
||||
{
|
||||
text: $_("add-to-list"),
|
||||
value: "list",
|
||||
icon: "bookmark",
|
||||
},
|
||||
]),
|
||||
...(isMultiselectMode() || !isFromCurrentUser()
|
||||
? []
|
||||
: [{ text: $_("share"), value: "share", icon: "share" }]),
|
||||
...(allowEdit()
|
||||
? [{ text: $_("edit"), value: "edit", icon: "pen" }]
|
||||
: []),
|
||||
...(allowDelete()
|
||||
? [{ text: $_("delete"), value: "delete", icon: "trash" }]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
{ text: $_("directions"), value: "direction", icon: "car" },
|
||||
...(trail.gpx
|
||||
? [
|
||||
{
|
||||
text: $_("export"),
|
||||
value: "download",
|
||||
icon: "download",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ text: $_("print"), value: "print", icon: "print" },
|
||||
...(isOwned
|
||||
? [{ text: $_("add-to-list"), value: "list", icon: "bookmark" }]
|
||||
: []),
|
||||
...(isOwned
|
||||
? [{ text: $_("share"), value: "share", icon: "share" }]
|
||||
: []),
|
||||
...(allowEdit
|
||||
? [{ text: $_("edit"), value: "edit", icon: "pen" }]
|
||||
: []),
|
||||
...(isOwned
|
||||
? [{ text: $_("delete"), value: "delete", icon: "trash" }]
|
||||
: []),
|
||||
];
|
||||
function isMultiselectMode(): boolean {
|
||||
return trails !== undefined && trails.size > 1;
|
||||
}
|
||||
|
||||
function hasTrail(): boolean {
|
||||
return (
|
||||
trails !== undefined &&
|
||||
trails.size > 0 &&
|
||||
[...trails][0] !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function hasGpx(): boolean {
|
||||
if (!hasTrail()) return false;
|
||||
|
||||
for (const gTrail of trails!) {
|
||||
if (gTrail.gpx) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function canExport(): boolean {
|
||||
return hasGpx();
|
||||
}
|
||||
|
||||
function trailId(): string | undefined {
|
||||
return trail()?.id;
|
||||
}
|
||||
|
||||
function getTrails(): Set<Trail> | undefined {
|
||||
return trails;
|
||||
}
|
||||
|
||||
function trail(): Trail | undefined {
|
||||
return hasTrail() ? [...trails!][0] : undefined;
|
||||
}
|
||||
|
||||
function isFromCurrentUser(uTrail?: Trail): boolean {
|
||||
if (uTrail !== undefined) {
|
||||
return uTrail.expand?.author?.id === $currentUser?.actor;
|
||||
} else if (trails !== undefined && trails.size > 0) {
|
||||
for (const sTrail of trails) {
|
||||
if (sTrail.expand?.author?.id === $currentUser?.actor) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function allowDelete(): boolean {
|
||||
return isFromCurrentUser();
|
||||
}
|
||||
|
||||
function allowDeleteTrail(dTrail?: Trail): boolean {
|
||||
return isFromCurrentUser(dTrail);
|
||||
}
|
||||
|
||||
async function handleDropdownClick(item: { text: string; value: any }) {
|
||||
if (!trail()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handle = handleFromRecordWithIRI(trail());
|
||||
|
||||
if (item.value == "show") {
|
||||
goto(
|
||||
mode == "overview"
|
||||
? `/map/trail/${handle}/${trail.id!}`
|
||||
: `/trail/view/${handle}/${trail.id!}`,
|
||||
);
|
||||
if (hasTrail()) {
|
||||
goto(
|
||||
mode == "overview" || mode == "multi-select"
|
||||
? `/map/trail/${handle}/${trailId()}`
|
||||
: `/trail/view/${handle}/${trailId()}`,
|
||||
);
|
||||
}
|
||||
} else if (item.value == "list") {
|
||||
lists = (
|
||||
await lists_index(
|
||||
@@ -95,79 +186,105 @@
|
||||
).items;
|
||||
listSelectModal.openModal();
|
||||
} else if (item.value == "direction") {
|
||||
window
|
||||
.open(
|
||||
`https://www.google.com/maps/dir/Current+Location/${trail.lat},${trail.lon}`,
|
||||
"_blank",
|
||||
)
|
||||
?.focus();
|
||||
if (hasTrail()) {
|
||||
window
|
||||
.open(
|
||||
`https://www.google.com/maps/dir/Current+Location/${trail()!.lat},${trail()!.lon}`,
|
||||
"_blank",
|
||||
)
|
||||
?.focus();
|
||||
}
|
||||
} else if (item.value == "print") {
|
||||
goto(`/map/trail/${handle}/${trail.id}/print`);
|
||||
if (hasTrail()) {
|
||||
goto(`/map/trail/${handle}/${trailId()}/print`);
|
||||
}
|
||||
} else if (item.value == "share") {
|
||||
trailShareModal.openModal();
|
||||
} else if (item.value == "download") {
|
||||
trailExportModal.openModal();
|
||||
} else if (item.value == "edit") {
|
||||
goto(`/trail/edit/${trail.id}`);
|
||||
if (hasTrail()) {
|
||||
goto(`/trail/edit/${trailId()}`);
|
||||
}
|
||||
} else if (item.value == "delete") {
|
||||
confirmModal.openModal();
|
||||
}
|
||||
}
|
||||
|
||||
async function exportTrail(exportSettings: {
|
||||
async function exportTrails(exportSettings: {
|
||||
fileFormat: "gpx" | "json";
|
||||
photos: boolean;
|
||||
summitLog: boolean;
|
||||
}) {
|
||||
try {
|
||||
let fileData: string = await trail2gpx(trail, $currentUser);
|
||||
if (exportSettings.fileFormat == "json") {
|
||||
fileData = JSON.stringify(
|
||||
gpx(
|
||||
new DOMParser().parseFromString(
|
||||
fileData,
|
||||
"application/gpx+xml" as any,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (trails !== undefined && trails.size > 0) {
|
||||
for (const cTrail of trails) {
|
||||
await doExportTrail(exportSettings, cTrail);
|
||||
}
|
||||
if (!exportSettings.photos && !exportSettings.summitLog) {
|
||||
const blob = new Blob([fileData], {
|
||||
type:
|
||||
exportSettings.fileFormat == "json"
|
||||
? "application/json"
|
||||
: "application/gpx+xml",
|
||||
});
|
||||
saveAs(blob, `${trail.name}.${exportSettings.fileFormat}`);
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
`${trail.name}.${exportSettings.fileFormat}`,
|
||||
fileData,
|
||||
);
|
||||
if (exportSettings.photos) {
|
||||
const photoFolder = zip.folder($_("photos"));
|
||||
for (const photo of trail.photos) {
|
||||
const photoURL = getFileURL(trail, photo);
|
||||
const photoBlob = await fetch(photoURL).then(
|
||||
(response) => response.blob(),
|
||||
);
|
||||
const photoData = new File([photoBlob], photo);
|
||||
photoFolder?.file(photo, photoData, { base64: true });
|
||||
}
|
||||
}
|
||||
if (exportSettings.summitLog) {
|
||||
let summitLogString = "";
|
||||
for (const summitLog of trail.expand?.summit_logs_via_trail ?? []) {
|
||||
summitLogString += `${summitLog.date},${summitLog.text}\n`;
|
||||
}
|
||||
zip.file(
|
||||
`${trail.name} - ${$_("summit-book")}.csv`,
|
||||
summitLogString,
|
||||
}
|
||||
}
|
||||
|
||||
async function doExportTrail(
|
||||
exportSettings: {
|
||||
fileFormat: "gpx" | "json";
|
||||
photos: boolean;
|
||||
summitLog: boolean;
|
||||
},
|
||||
eTrail: Trail,
|
||||
) {
|
||||
try {
|
||||
if (eTrail !== undefined) {
|
||||
let fileData: string = await trail2gpx(eTrail, $currentUser);
|
||||
if (exportSettings.fileFormat == "json") {
|
||||
fileData = JSON.stringify(
|
||||
gpx(
|
||||
new DOMParser().parseFromString(
|
||||
fileData,
|
||||
"application/gpx+xml" as any,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
const blob = await zip.generateAsync({ type: "blob" });
|
||||
saveAs(blob, `${trail.name}.zip`);
|
||||
if (!exportSettings.photos && !exportSettings.summitLog) {
|
||||
const blob = new Blob([fileData], {
|
||||
type:
|
||||
exportSettings.fileFormat == "json"
|
||||
? "application/json"
|
||||
: "application/gpx+xml",
|
||||
});
|
||||
saveAs(blob, `${eTrail.name}.${exportSettings.fileFormat}`);
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
`${eTrail.name}.${exportSettings.fileFormat}`,
|
||||
fileData,
|
||||
);
|
||||
if (exportSettings.photos) {
|
||||
const photoFolder = zip.folder($_("photos"));
|
||||
for (const photo of eTrail.photos) {
|
||||
const photoURL = getFileURL(eTrail, photo);
|
||||
const photoBlob = await fetch(photoURL).then(
|
||||
(response) => response.blob(),
|
||||
);
|
||||
const photoData = new File([photoBlob], photo);
|
||||
photoFolder?.file(photo, photoData, {
|
||||
base64: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (exportSettings.summitLog) {
|
||||
let summitLogString = "";
|
||||
for (const summitLog of eTrail.expand
|
||||
?.summit_logs_via_trail ?? []) {
|
||||
summitLogString += `${summitLog.date},${summitLog.text}\n`;
|
||||
}
|
||||
zip.file(
|
||||
`${eTrail.name} - ${$_("summit-book")}.csv`,
|
||||
summitLogString,
|
||||
);
|
||||
}
|
||||
const blob = await zip.generateAsync({ type: "blob" });
|
||||
saveAs(blob, `${eTrail.name}.zip`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -179,28 +296,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTrail() {
|
||||
await trails_delete(trail);
|
||||
setTimeout(() => {
|
||||
goto("/trails");
|
||||
}, 500);
|
||||
async function deleteTrails() {
|
||||
if (hasTrail()) {
|
||||
for (const dTrail of trails!) {
|
||||
await doDeleteTrail(dTrail);
|
||||
}
|
||||
|
||||
onconfirm?.(true);
|
||||
}
|
||||
}
|
||||
|
||||
async function doDeleteTrail(dTrail: Trail) {
|
||||
if (dTrail === undefined) return;
|
||||
|
||||
if (!allowDeleteTrail(dTrail)) return;
|
||||
|
||||
await trails_delete(dTrail);
|
||||
}
|
||||
|
||||
async function handleShareUpdate() {
|
||||
onconfirm?.();
|
||||
}
|
||||
|
||||
async function handleListSelection(list: List) {
|
||||
try {
|
||||
if (list.trails?.includes(trail.id!)) {
|
||||
await lists_remove_trail(list, trail);
|
||||
let deleted = false;
|
||||
let multiple = false;
|
||||
|
||||
if (hasTrail()) {
|
||||
multiple = true;
|
||||
for (const lTrail of trails!) {
|
||||
if (await doHandleListSelection(list, lTrail)) {
|
||||
deleted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deleted) {
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: `${$_("removed-trail-from")} "${list.name}"`,
|
||||
text: multiple
|
||||
? `${$_("removed-trails-from")} "${list.name}"`
|
||||
: `${$_("removed-trail-from")} "${list.name}"`,
|
||||
});
|
||||
} else {
|
||||
await lists_add_trail(list, trail);
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: `${$_("added-trail-to")} "${list.name}"`,
|
||||
text: multiple
|
||||
? `${$_("added-trails-to")} "${list.name}"`
|
||||
: `${$_("added-trail-to")} "${list.name}"`,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -213,32 +359,79 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function doHandleListSelection(
|
||||
list: List,
|
||||
lTrail: Trail,
|
||||
): Promise<boolean> {
|
||||
if (list.trails?.includes(lTrail.id!)) {
|
||||
if (listContainsAllTrails(list)) {
|
||||
await lists_remove_trail(list, lTrail);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
await lists_add_trail(list, lTrail);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function listContainsAllTrails(list: List): boolean {
|
||||
if (trails === undefined) {
|
||||
return false;
|
||||
} else if (list.trails !== undefined) {
|
||||
for (const lTrail of trails) {
|
||||
if (!list.trails!.includes(lTrail.id!)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dropdown items={dropdownItems} onchange={(item) => handleDropdownClick(item)}
|
||||
<Dropdown items={dropdownItems()} onchange={(item) => handleDropdownClick(item)}
|
||||
>{#snippet children({ toggleMenu: openDropdown })}
|
||||
<button
|
||||
aria-label="Open dropdown"
|
||||
class=" btn-primary !rounded-full h-12 w-12"
|
||||
onclick={openDropdown}
|
||||
>
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button>
|
||||
{#if mode == "multi-select"}
|
||||
<button
|
||||
aria-label="Open dropdown"
|
||||
class="btn-primary flex-shrink-0 !font-medium"
|
||||
onclick={openDropdown}
|
||||
>
|
||||
<span
|
||||
>{trails?.size}
|
||||
{$_("selected")} <i class="fa fa-caret-down ml-1"></i></span
|
||||
>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
aria-label="Open dropdown"
|
||||
class=" btn-primary !rounded-full h-12 w-12"
|
||||
onclick={openDropdown}
|
||||
>
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Dropdown>
|
||||
|
||||
<ConfirmModal
|
||||
text={$_("delete-trail-confirm")}
|
||||
bind:this={confirmModal}
|
||||
onconfirm={deleteTrail}
|
||||
onconfirm={deleteTrails}
|
||||
></ConfirmModal>
|
||||
<ListSelectModal
|
||||
{lists}
|
||||
trails={getTrails()}
|
||||
bind:this={listSelectModal}
|
||||
onchange={(list) => handleListSelection(list)}
|
||||
></ListSelectModal>
|
||||
<TrailExportModal
|
||||
bind:this={trailExportModal}
|
||||
onexport={(settings) => exportTrail(settings)}
|
||||
onexport={(settings) => exportTrails(settings)}
|
||||
></TrailExportModal>
|
||||
<TrailShareModal {trail} bind:this={trailShareModal}></TrailShareModal>
|
||||
<TrailShareModal
|
||||
trail={trail()}
|
||||
onsave={handleShareUpdate}
|
||||
bind:this={trailShareModal}
|
||||
></TrailShareModal>
|
||||
|
||||
@@ -401,7 +401,10 @@
|
||||
{#if ($currentUser && $currentUser.actor == trail.author) || trail.expand?.trail_share_via_trail?.length || trail.public}
|
||||
<div class="flex flex-col items-center gap-y-2">
|
||||
<LikeButton {trail}></LikeButton>
|
||||
<TrailDropdown {trail} {mode} {handle}></TrailDropdown>
|
||||
<TrailDropdown
|
||||
trails={new Set<Trail>([trail])}
|
||||
{mode}
|
||||
></TrailDropdown>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -658,7 +661,7 @@
|
||||
elevationProfileContainer={"epc-container"}
|
||||
showStyleSwitcher={false}
|
||||
showFullscreen={true}
|
||||
mapOptions={{ attributionControl: {compact: true} }}
|
||||
mapOptions={{ attributionControl: { compact: true } }}
|
||||
onfullscreen={toggleMapFullScreen}
|
||||
bind:markers
|
||||
></MapWithElevationMaplibre>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import SkeletonCard from "../base/skeleton_card.svelte";
|
||||
import SkeletonListItem from "../base/skeleton_list_item.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import TrailDropdown from "$lib/components/trail/trail_dropdown.svelte";
|
||||
|
||||
interface Props {
|
||||
filter?: TrailFilter | null;
|
||||
@@ -17,7 +18,10 @@
|
||||
pagination?: { page: number; totalPages: number };
|
||||
loading?: boolean;
|
||||
fullWidthCards?: boolean;
|
||||
onupdate?: (filter: TrailFilter | null) => void;
|
||||
onupdate?: (
|
||||
filter: TrailFilter | null,
|
||||
selection: Set<Trail> | undefined,
|
||||
) => void;
|
||||
onpagination?: (page: number) => void;
|
||||
}
|
||||
|
||||
@@ -42,6 +46,9 @@
|
||||
|
||||
let selectedDisplayOption = $state(displayOptions[0].value);
|
||||
|
||||
let selection: Set<Trail> | undefined = $state();
|
||||
let hoveredTrail: Trail | undefined = $state();
|
||||
|
||||
const sortOptions: SelectItem[] = [
|
||||
{ text: $_("name"), value: "name" },
|
||||
{ text: $_("distance"), value: "distance" },
|
||||
@@ -71,7 +78,7 @@
|
||||
(storedSortOrder as typeof filter.sortOrder | null) ??
|
||||
filter.sortOrder;
|
||||
}
|
||||
onupdate?.(filter);
|
||||
onupdate?.(filter, selection);
|
||||
});
|
||||
|
||||
function setDisplayOption() {
|
||||
@@ -83,7 +90,7 @@
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("sort", filter.sort);
|
||||
onupdate?.(filter);
|
||||
onupdate?.(filter, selection);
|
||||
}
|
||||
|
||||
function setSortOrder() {
|
||||
@@ -96,7 +103,7 @@
|
||||
filter.sortOrder = "+";
|
||||
}
|
||||
localStorage.setItem("sort_order", filter.sortOrder);
|
||||
onupdate?.(filter);
|
||||
onupdate?.(filter, selection);
|
||||
}
|
||||
|
||||
function handleSortUpdate(sort: any) {
|
||||
@@ -111,6 +118,96 @@
|
||||
setSort();
|
||||
}
|
||||
}
|
||||
|
||||
function isHovered(trail: Trail): boolean {
|
||||
if (trail === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hoveredTrail === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hoveredTrail.id === trail.id;
|
||||
}
|
||||
|
||||
function isSelected(trail: Trail): boolean {
|
||||
if (trail === undefined) {
|
||||
return false;
|
||||
} else {
|
||||
if (selection === undefined) {
|
||||
return false;
|
||||
} else {
|
||||
for (const sTrail of selection) {
|
||||
if (sTrail !== undefined && sTrail.id === trail.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleSelectionUpdate(trail: Trail) {
|
||||
let newSelection = new Set<Trail>();
|
||||
|
||||
if (trail !== undefined) {
|
||||
let isSelected = false;
|
||||
|
||||
if (selection !== undefined && selection.size > 0) {
|
||||
for (const sTrail of selection) {
|
||||
if (sTrail !== undefined && sTrail.id === trail.id) {
|
||||
isSelected = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
newSelection.add(sTrail);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSelected) {
|
||||
newSelection.add(trail);
|
||||
}
|
||||
} else if (
|
||||
selection === undefined ||
|
||||
selection.size === 0 ||
|
||||
(trails !== undefined && selection.size !== trails.length)
|
||||
) {
|
||||
for (const eTrail of trails) {
|
||||
newSelection.add(eTrail);
|
||||
}
|
||||
}
|
||||
|
||||
selection = newSelection;
|
||||
}
|
||||
|
||||
function handleHoverUpdate(hTrail: Trail) {
|
||||
if (hTrail === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hoveredTrail === undefined) hoveredTrail = hTrail;
|
||||
else hoveredTrail = undefined;
|
||||
}
|
||||
|
||||
function handleTrailsEditDone(resetSelection: boolean = false) {
|
||||
if (resetSelection) {
|
||||
selection?.clear();
|
||||
hoveredTrail = undefined;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
onupdate?.(filter, selection);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function handleMouseEnter(trail: Trail) {
|
||||
handleHoverUpdate(trail);
|
||||
}
|
||||
function handleMouseLeave(trail: Trail) {
|
||||
handleHoverUpdate(trail);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-w-0">
|
||||
@@ -122,6 +219,15 @@
|
||||
{onpagination}
|
||||
></Pagination>
|
||||
</div>
|
||||
{#if selection !== undefined && selection.size > 0}
|
||||
<div class="flex relative flex-shrink-0">
|
||||
<TrailDropdown
|
||||
trails={selection}
|
||||
mode={"multi-select"}
|
||||
onconfirm={handleTrailsEditDone}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if filter}
|
||||
<div class="shrink-0">
|
||||
{#if selectedDisplayOption !== "table"}
|
||||
@@ -158,7 +264,10 @@
|
||||
<div id="trails" class="flex items-start flex-wrap gap-8 py-8 max-w-full">
|
||||
{#if loading}
|
||||
{#if selectedDisplayOption === "table"}
|
||||
<TrailTable trails={null} tableHeader={sortOptions}
|
||||
<TrailTable
|
||||
trails={null}
|
||||
selection={new Set<Trail>()}
|
||||
tableHeader={sortOptions}
|
||||
></TrailTable>
|
||||
{:else}
|
||||
{#each { length: 12 } as _, index}
|
||||
@@ -180,11 +289,13 @@
|
||||
{#if selectedDisplayOption === "table"}
|
||||
<TrailTable
|
||||
{trails}
|
||||
{selection}
|
||||
tableHeader={sortOptions.filter(
|
||||
(option) => option.value !== "elevation_loss",
|
||||
)}
|
||||
{filter}
|
||||
onsort={handleSortUpdate}
|
||||
onTrailSelect={(t) => handleSelectionUpdate(t)}
|
||||
></TrailTable>
|
||||
{:else}
|
||||
{#each trails as trail}
|
||||
@@ -194,12 +305,26 @@
|
||||
href="/trail/view/@{trail.author}{trail.domain
|
||||
? `@${trail.domain}`
|
||||
: ''}/{trail.id}"
|
||||
onmouseenter={(e) => handleMouseEnter(trail)}
|
||||
onmouseleave={(e) => handleMouseLeave(trail)}
|
||||
>
|
||||
{#if selectedDisplayOption === "cards"}
|
||||
<TrailCard fullWidth={fullWidthCards} {trail}
|
||||
<TrailCard
|
||||
fullWidth={fullWidthCards}
|
||||
{trail}
|
||||
selected={isSelected(trail)}
|
||||
hovered={isHovered(trail)}
|
||||
onTrailSelect={() =>
|
||||
handleSelectionUpdate(trail)}
|
||||
></TrailCard>
|
||||
{:else}
|
||||
<TrailListItem {trail}></TrailListItem>
|
||||
<TrailListItem
|
||||
{trail}
|
||||
selected={isSelected(trail)}
|
||||
hovered={isHovered(trail)}
|
||||
onTrailSelect={() =>
|
||||
handleSelectionUpdate(trail)}
|
||||
></TrailListItem>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
@@ -19,9 +19,18 @@
|
||||
interface Props {
|
||||
trail: Trail;
|
||||
showDescription?: boolean;
|
||||
selected: boolean;
|
||||
hovered: boolean;
|
||||
onTrailSelect?: () => void;
|
||||
}
|
||||
|
||||
let { trail, showDescription = true }: Props = $props();
|
||||
let {
|
||||
trail,
|
||||
showDescription = true,
|
||||
selected = false,
|
||||
hovered = false,
|
||||
onTrailSelect,
|
||||
}: Props = $props();
|
||||
|
||||
let thumbnail = $derived(
|
||||
trail.photos.length
|
||||
@@ -35,6 +44,12 @@
|
||||
: emptyStateTrailDark,
|
||||
);
|
||||
|
||||
function handleInputClick(e: Event) {
|
||||
e.stopPropagation();
|
||||
onTrailSelect?.();
|
||||
hovered = true;
|
||||
}
|
||||
|
||||
let expandedTags = $state(false);
|
||||
|
||||
function toggleExpandTags(e: MouseEvent) {
|
||||
@@ -64,7 +79,7 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-w-0 basis-full">
|
||||
<div class="min-w-0 basis-full relative">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-semibold text-lg">
|
||||
{trail.name}
|
||||
@@ -139,9 +154,6 @@
|
||||
{#if trail.location}
|
||||
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
||||
{/if}
|
||||
<h5>
|
||||
<i class="fa fa-gauge mr-3"></i>{$_(trail.difficulty ?? "?")}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap mt-1 gap-x-4 gap-y-2 text-sm text-gray-500">
|
||||
@@ -168,10 +180,23 @@
|
||||
</div>
|
||||
{#if showDescription}
|
||||
<p
|
||||
class="mt-3 text-sm whitespace-nowrap min-w-0 max-w-full overflow-hidden text-ellipsis"
|
||||
class="mt-3 text-sm whitespace-nowrap min-w-0 max-w-full overflow-hidden text-ellipsis basis-full"
|
||||
>
|
||||
{formatHTMLAsText(trail.description ?? "")}
|
||||
</p>
|
||||
{/if}
|
||||
{#if hovered || selected}
|
||||
<div
|
||||
class="flex absolute bottom-0 right-0 w-8 h-8 rounded-full items-center justify-center bg-background text-content"
|
||||
>
|
||||
<input
|
||||
id="trail-selected"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||
bind:checked={selected}
|
||||
onclick={(e) => handleInputClick(e)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import Select from "../base/select.svelte";
|
||||
|
||||
interface Props {
|
||||
trail: Trail;
|
||||
trail?: Trail;
|
||||
onsave?: () => void;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@
|
||||
}
|
||||
|
||||
async function shareTrail(item: SelectItem) {
|
||||
if (trail === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.value.isLocal && !trail.public) {
|
||||
displayShareError = true;
|
||||
return;
|
||||
@@ -80,6 +84,8 @@
|
||||
}
|
||||
|
||||
async function fetchShares() {
|
||||
if (trail === undefined) return;
|
||||
|
||||
sharesLoading = true;
|
||||
await trail_share_index({ trail: trail.id! });
|
||||
sharesLoading = false;
|
||||
|
||||
@@ -10,18 +10,22 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import ShareInfo from "../share_info.svelte";
|
||||
|
||||
|
||||
interface Props {
|
||||
tableHeader: SelectItem[];
|
||||
trails?: Trail[] | null;
|
||||
selection: Set<Trail> | undefined;
|
||||
filter?: TrailFilter | null;
|
||||
onsort?: (value: any) => void;
|
||||
onsort?: (value: any) => void
|
||||
onTrailSelect?: (value: any) => void
|
||||
}
|
||||
|
||||
let { tableHeader, trails = null, filter = null, onsort }: Props = $props();
|
||||
let { tableHeader, trails = null, selection, filter = null, onsort, onTrailSelect: onselect }: Props = $props();
|
||||
|
||||
function getColumnWidth(columnValue: string): string {
|
||||
switch (columnValue) {
|
||||
case "select":
|
||||
return "w-[2%]";
|
||||
case "name":
|
||||
return "w-[25%]";
|
||||
case "distance":
|
||||
@@ -37,14 +41,68 @@
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedTrail(e: Event, trail: Trail) {
|
||||
e.stopPropagation()
|
||||
|
||||
if (trail !== undefined) {
|
||||
if (onselect !== undefined) {
|
||||
onselect(trail)
|
||||
}
|
||||
else {
|
||||
console.error("undefined event handler")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedAllTrails(e: Event) {
|
||||
onselect?.(undefined)
|
||||
}
|
||||
|
||||
function isSelected(trail: Trail): boolean {
|
||||
if (selection === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trail !== undefined) {
|
||||
for (const strail of selection) {
|
||||
if (strail !== undefined && strail.id === trail.id)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function allSelected(): boolean {
|
||||
if (selection === undefined || trails === undefined || trails === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selection.size === trails.length;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="table-container w-full border border-input-border rounded-xl overflow-x-scroll overflow-y-clip"
|
||||
class="table-container w-full border border-input-border rounded-xl overflow-x-auto overflow-y-clip"
|
||||
>
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="bg-secondary-hover">
|
||||
<th
|
||||
class="p-4 text-left text-sm font-medium {getColumnWidth("select")}"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
id="trail-selected"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||
onclick={setSelectedAllTrails}
|
||||
checked={allSelected()}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
{#each tableHeader as column}
|
||||
<th
|
||||
class="p-4 text-left text-sm font-medium {getColumnWidth(
|
||||
@@ -78,6 +136,16 @@
|
||||
}/${trail.id}`,
|
||||
)}
|
||||
>
|
||||
<td class="p-4 text-sm">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
|
||||
checked={isSelected(trail)}
|
||||
onclick={(e: Event) => setSelectedTrail(e, trail)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
class="flex justify-between items-center text-sm relative"
|
||||
>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "Zu Liste hinzufügen",
|
||||
"add-waypoint": "Wegpunkt hinzufügen",
|
||||
"added-trail-to": "Route hinzugefügt zu",
|
||||
"added-trails-to": "Routen hinzugefügt zu",
|
||||
"after": "Nach",
|
||||
"all-activities": "Alle Aktivitäten",
|
||||
"alphabetical": "Alphabetisch",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "Registrieren",
|
||||
"remote-users-cannot-edit": "Nutzer anderer Instanzen können nicht bearbeiten",
|
||||
"removed-trail-from": "Route entfernt aus",
|
||||
"removed-trails-from": "Routen entfernt aus",
|
||||
"required": "Pflichtfeld",
|
||||
"reset-password": "Passwort zurücksetzen",
|
||||
"road": "Straße",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Orte suchen",
|
||||
"search-trails": "Route suchen",
|
||||
"select-list": "Liste auswählen",
|
||||
"selected": "selected",
|
||||
"settings": "Einstellungen",
|
||||
"settings-notification-comment-mention": "Jemand hat dich in einem Kommentar erwähnt",
|
||||
"settings-notification-list-create": "Ein Benutzer, dem Du folgst, hat eine Liste erstellt",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "Add to list",
|
||||
"add-waypoint": "Add Waypoint",
|
||||
"added-trail-to": "Added trail to",
|
||||
"added-trails-to": "Added trails to",
|
||||
"after": "After",
|
||||
"all-activities": "All activities",
|
||||
"alphabetical": "Alphabetical",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "Register",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Removed trail from",
|
||||
"removed-trails-from": "Removed trails from",
|
||||
"required": "Required",
|
||||
"reset-password": "Reset Password",
|
||||
"road": "Road",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Search places",
|
||||
"search-trails": "Search trails",
|
||||
"select-list": "Select List",
|
||||
"selected": "",
|
||||
"settings": "Settings",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "Añadir a la lista",
|
||||
"add-waypoint": "Añadir Punto de Interés",
|
||||
"added-trail-to": "Ruta añadida a",
|
||||
"added-trails-to": "Rutas añadida a",
|
||||
"after": "Después",
|
||||
"all-activities": "Todas las actividades",
|
||||
"alphabetical": "Alfabético",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "Registrar",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Ruta borrada de",
|
||||
"removed-trails-from": "Rutas borrada de",
|
||||
"required": "Obligatorio",
|
||||
"reset-password": "Restablecer Contraseña",
|
||||
"road": "Carretera",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Buscar lugares",
|
||||
"search-trails": "Buscar ruta",
|
||||
"select-list": "Seleccionar Lista",
|
||||
"selected": "",
|
||||
"settings": "Configuración",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Un usuario al que sigues ha creado una nueva lista",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "Ajouter à une liste",
|
||||
"add-waypoint": "Ajouter un point de passage",
|
||||
"added-trail-to": "Ajouter un itinéraire à",
|
||||
"added-trails-to": "Ajouter les itinéraires à",
|
||||
"after": "Après",
|
||||
"all-activities": "Toutes les activités",
|
||||
"alphabetical": "Alphabétique",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "Créer un compte",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Enlever l'itinéraire de",
|
||||
"removed-trails-from": "Enlever les itinéraires de",
|
||||
"required": "Requis",
|
||||
"reset-password": "Réinitialiser le mot de passe",
|
||||
"road": "Road",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Chercher des lieux",
|
||||
"search-trails": "Chercher un itinéraire",
|
||||
"select-list": "Liste de choix",
|
||||
"selected": "",
|
||||
"settings": "Paramètres",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Un utilisateur que vous suivez à créé une nouvelle liste",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "Hozzáadás a listához",
|
||||
"add-waypoint": "Útvonalpont hozzáadása",
|
||||
"added-trail-to": "Hozzáadott nyomvonal a",
|
||||
"added-trails-to": "Hozzáadott nyomvonalak a",
|
||||
"after": "After",
|
||||
"all-activities": "All activities",
|
||||
"alphabetical": "Betűrendben",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "Regisztráció",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Eltávolított nyomvonal a",
|
||||
"removed-trails-from": "Eltávolított nyomvonalak a",
|
||||
"required": "Kötelező",
|
||||
"reset-password": "Reset Password",
|
||||
"road": "Road",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Search places",
|
||||
"search-trails": "Nyomvonalak keresése",
|
||||
"select-list": "Lista kiválasztása",
|
||||
"selected": "",
|
||||
"settings": "Beállítások",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "Aggiungi alla lista",
|
||||
"add-waypoint": "Aggiungi un punto di passaggio",
|
||||
"added-trail-to": "Percorso aggiunto a",
|
||||
"added-trails-to": "Percorsi aggiunto a",
|
||||
"after": "Dopo",
|
||||
"all-activities": "Tutte le Attività",
|
||||
"alphabetical": "Alfabetico",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "Registrati",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Percorso rimosso da",
|
||||
"removed-trails-from": "Percorsi rimosso da",
|
||||
"required": "Obbligatorio",
|
||||
"reset-password": "Ripristinare Password",
|
||||
"road": "Road",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Search places",
|
||||
"search-trails": "Cerca percorsi",
|
||||
"select-list": "Seleziona lista",
|
||||
"selected": "",
|
||||
"settings": "Impostazioni",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Un utente che segui ha creato una lista",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "Toevoegen aan lijst",
|
||||
"add-waypoint": "Routepunt toevoegen",
|
||||
"added-trail-to": "Route toegevoegd aan",
|
||||
"added-trails-to": "Route toegevoegd aan",
|
||||
"after": "Na",
|
||||
"all-activities": "Alle activiteiten",
|
||||
"alphabetical": "Alfabetisch",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "Registreren",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Route verwijderd van",
|
||||
"removed-trails-from": "Routes verwijderd van",
|
||||
"required": "Verplicht",
|
||||
"reset-password": "Wachtwoord opnieuw instellen",
|
||||
"road": "Weg",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Zoek plaatsen",
|
||||
"search-trails": "Zoek routes",
|
||||
"select-list": "Kies een lijst",
|
||||
"selected": "",
|
||||
"settings": "Instellingen",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Een gebruiker die je volgt, heeft een lijst gecreëerd",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "Dodaj do listy",
|
||||
"add-waypoint": "Dodaj Punkt",
|
||||
"added-trail-to": "Dodaj szlak do",
|
||||
"added-trails-to": "Dodaj szlaki do",
|
||||
"after": "Po",
|
||||
"all-activities": "Wszystkie aktywności",
|
||||
"alphabetical": "Alfabetyczne",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "Zarejestruj",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Usunięto szlak z",
|
||||
"removed-trails-from": "Usunięto szlaki z",
|
||||
"required": "Wymagane",
|
||||
"reset-password": "Resetuj hasło",
|
||||
"road": "Road",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Szukaj miejsc",
|
||||
"search-trails": "Szukaj szlaków",
|
||||
"select-list": "Wybierz Listę",
|
||||
"selected": "",
|
||||
"settings": "Ustawienia",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Użytkownik, którego obserwujesz, utworzył listę",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "Adicionar à lista",
|
||||
"add-waypoint": "Adicionar ponto de vista",
|
||||
"added-trail-to": "Trilha adicionada para",
|
||||
"added-trails-to": "trilhas adicionada para",
|
||||
"after": "Depois",
|
||||
"all-activities": "Todas as atividades",
|
||||
"alphabetical": "Alfabético",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "Registo",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "Trilha removida de",
|
||||
"removed-trails-from": "Trilhos removidos de",
|
||||
"required": "Obrigatório",
|
||||
"reset-password": "Reset Password",
|
||||
"road": "Road",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Search places",
|
||||
"search-trails": "Procurar trilhos",
|
||||
"select-list": "Selecionar lista",
|
||||
"selected": "",
|
||||
"settings": "Definições",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"add-to-list": "添加到列表",
|
||||
"add-waypoint": "添加坐标",
|
||||
"added-trail-to": "添加路线到",
|
||||
"added-trails-to": "添加路线到",
|
||||
"after": "之后",
|
||||
"all-activities": "所有活动",
|
||||
"alphabetical": "字母",
|
||||
@@ -275,6 +276,7 @@
|
||||
"register": "注册",
|
||||
"remote-users-cannot-edit": "",
|
||||
"removed-trail-from": "路线已删除自",
|
||||
"removed-trails-from": "路线已删除自",
|
||||
"required": "必填",
|
||||
"reset-password": "重置密码",
|
||||
"road": "Road",
|
||||
@@ -289,6 +291,7 @@
|
||||
"search-places": "Search places",
|
||||
"search-trails": "搜索路线",
|
||||
"select-list": "选择列表",
|
||||
"selected": "",
|
||||
"settings": "设置",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
|
||||
@@ -14,6 +14,8 @@ import JSZip from "jszip";
|
||||
import type { AuthRecord } from "pocketbase";
|
||||
import * as xmldom from 'xmldom';
|
||||
import { bbox, splitMultiLineStringToLineStrings } from "./geojson_util";
|
||||
import { trails_show } from "$lib/stores/trail_store";
|
||||
import { handleFromRecordWithIRI } from "./activitypub_util";
|
||||
|
||||
|
||||
export async function gpx2trail(gpxString: string, fallbackName?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
@@ -71,10 +73,21 @@ export async function gpx2trail(gpxString: string, fallbackName?: string, f: (ur
|
||||
}
|
||||
|
||||
export async function trail2gpx(trail: Trail, user?: AuthRecord) {
|
||||
let gpxTrail = trail;
|
||||
|
||||
if (!trail.expand?.gpx_data) {
|
||||
throw Error("Trail has no GPX data")
|
||||
// no gpx_data -> empty trail?
|
||||
// or just not expanded? -> expand now
|
||||
const response = await trails_show(trail.id!, handleFromRecordWithIRI(trail), true);
|
||||
|
||||
if (!response.expand?.gpx_data) {
|
||||
throw Error("Trail has no GPX data")
|
||||
} else {
|
||||
gpxTrail = response;
|
||||
}
|
||||
}
|
||||
const gpx = await GPX.parse(trail.expand.gpx_data) as GPX;
|
||||
|
||||
const gpx = await GPX.parse(gpxTrail.expand!.gpx_data!) as GPX;
|
||||
|
||||
if (gpx instanceof Error) {
|
||||
throw gpx;
|
||||
@@ -92,7 +105,7 @@ export async function trail2gpx(trail: Trail, user?: AuthRecord) {
|
||||
gpx.wpt = [];
|
||||
}
|
||||
|
||||
for (const wp of trail.expand.waypoints ?? []) {
|
||||
for (const wp of gpxTrail.expand!.waypoints ?? []) {
|
||||
const gpxWpt = gpx.wpt.find((w) => w.$.lat == wp.lat && w.$.lon == wp.lon)
|
||||
if (!gpxWpt) {
|
||||
gpx.wpt.push({
|
||||
|
||||
Reference in New Issue
Block a user