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:
slothful-vassal
2025-06-26 15:28:27 +02:00
committed by GitHub
parent 9b6522ec21
commit 06f6d58b5e
22 changed files with 666 additions and 163 deletions

View File

@@ -59,7 +59,7 @@ func GenerateHTML(appUrl string, recipientName string, authorName string, notifi
} }
html, err := registry.LoadFiles( html, err := registry.LoadFiles(
"templates/mail/notification.html", "db/templates/mail/notification.html",
).Render(content) ).Render(content)
if err != nil { if err != nil {

View File

@@ -34,7 +34,7 @@
try { try {
const actors: Actor[] = await searchActors(q, includeSelf); const actors: Actor[] = await searchActors(q, includeSelf);
searchItems = actors.map((a) => ({ searchItems = actors.map((a) => ({
text: "a.username!", text: a.username,
description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`, description: `@${a.preferred_username}${a.isLocal ? "" : "@" + a.domain}`,
value: a, value: a,
icon: "user", icon: "user",

View File

@@ -2,6 +2,7 @@
import { type Snippet } from "svelte"; import { type Snippet } from "svelte";
import type { List } from "$lib/models/list"; import type { List } from "$lib/models/list";
import type { Trail } from "$lib/models/trail";
import { trail } from "$lib/stores/trail_store"; import { trail } from "$lib/stores/trail_store";
import { getFileURL } from "$lib/util/file_util"; import { getFileURL } from "$lib/util/file_util";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
@@ -12,11 +13,12 @@
interface Props { interface Props {
lists: List[]; lists: List[];
trails?: Set<Trail> | undefined;
children?: Snippet<[any]>; children?: Snippet<[any]>;
onchange?: (list: List) => void onchange?: (list: List) => void
} }
let { lists, children, onchange }: Props = $props(); let { lists, trails, children, onchange }: Props = $props();
let modal: Modal; let modal: Modal;
@@ -29,6 +31,20 @@
modal.closeModal!(); 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) { function listContainsCurrentTrail(list: List) {
return list.trails?.includes($trail.id!); return list.trails?.includes($trail.id!);
} }
@@ -66,7 +82,7 @@
<h5 class="text-md font-semibold">{list.name}</h5> <h5 class="text-md font-semibold">{list.name}</h5>
<i <i
class="fa fa-{listContainsCurrentTrail(list) class="fa fa-{listContainsAllTrails(list)
? 'minus' ? 'minus'
: 'plus'} rounded-full border border-input-border p-2" : 'plus'} rounded-full border border-input-border p-2"
></i> ></i>

View File

@@ -18,15 +18,21 @@
interface Props { interface Props {
trail: Trail; trail: Trail;
fullWidth?: boolean; fullWidth?: boolean;
selected: boolean;
hovered: boolean;
onmouseenter?: MouseEventHandler<HTMLDivElement>; onmouseenter?: MouseEventHandler<HTMLDivElement>;
onmouseleave?: MouseEventHandler<HTMLDivElement>; onmouseleave?: MouseEventHandler<HTMLDivElement>;
onTrailSelect?: () => void;
} }
let { let {
trail, trail,
fullWidth = false, fullWidth = false,
selected = false,
hovered = false,
onmouseenter, onmouseenter,
onmouseleave, onmouseleave,
onTrailSelect,
}: Props = $props(); }: Props = $props();
let thumbnail = $derived( let thumbnail = $derived(
@@ -45,6 +51,11 @@
(trail.expand?.trail_share_via_trail?.length ?? 0) > 0, (trail.expand?.trail_share_via_trail?.length ?? 0) > 0,
); );
function handleInputClick(e: Event) {
e.stopPropagation();
onTrailSelect?.();
hovered = true;
}
// expand and collapse the tags // expand and collapse the tags
let expandedTags = $state(false); let expandedTags = $state(false);
@@ -85,18 +96,17 @@
/> />
{/if} {/if}
</div> </div>
{#if $currentUser && trail.like_count > 0} {#if hovered || selected}
<div <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")}> <input
<i class="fa fa-heart"></i> id="trail-selected"
</span> type="checkbox"
<div class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
class="absolute pointer-events-none left-5 -top-1 text-xs rounded-full bg-menu-background px-1 text-center" bind:checked={selected}
> onclick={(e) => handleInputClick(e)}
{trail.like_count} />
</div>
</div> </div>
{/if} {/if}
{#if (trail.public || trailIsShared) && $currentUser} {#if (trail.public || trailIsShared) && $currentUser}
@@ -121,6 +131,20 @@
{/if} {/if}
</div> </div>
{/if} {/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 class="p-4">
<div> <div>
<h4 class="font-semibold text-lg line-clamp-2">{trail.name}</h4> <h4 class="font-semibold text-lg line-clamp-2">{trail.name}</h4>
@@ -162,9 +186,9 @@
type="button" type="button"
> >
{#if expandedTags} {#if expandedTags}
{$_('show-less')} {$_("show-less")}
{:else} {:else}
+{trail.tags.length - 2} {$_('more')} +{trail.tags.length - 2} {$_("more")}
{/if} {/if}
</button> </button>
{/if} {/if}

View File

@@ -20,14 +20,15 @@
import ListSelectModal from "../list/list_select_modal.svelte"; import ListSelectModal from "../list/list_select_modal.svelte";
import TrailExportModal from "./trail_export_modal.svelte"; import TrailExportModal from "./trail_export_modal.svelte";
import TrailShareModal from "./trail_share_modal.svelte"; import TrailShareModal from "./trail_share_modal.svelte";
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
interface Props { interface Props {
trail: Trail; trails?: Set<Trail> | undefined;
handle: string; mode: "overview" | "map" | "list" | "multi-select";
mode: "overview" | "map" | "list"; onconfirm?: (resetSelection?: boolean) => void;
} }
let { trail, handle, mode }: Props = $props(); let { trails, mode, onconfirm }: Props = $props();
let confirmModal: ConfirmModal; let confirmModal: ConfirmModal;
let listSelectModal: ListSelectModal; let listSelectModal: ListSelectModal;
@@ -36,25 +37,38 @@
let lists: List[] = $state([]); let lists: List[] = $state([]);
const isOwned: boolean = trail.author == $currentUser?.actor; function allowEdit(): boolean {
return (
const allowEdit = hasTrail() &&
isOwned || !isMultiselectMode() &&
trail.expand?.trail_share_via_trail?.some( (trail()!.expand?.author?.id === $currentUser?.actor ||
trail()!.expand?.trail_share_via_trail?.some(
(s) => s.permission == "edit", (s) => s.permission == "edit",
))!
); );
}
const dropdownItems: DropdownItem[] = [ function dropdownItems(): DropdownItem[] {
mode == "overview" return [
? { text: $_("show-on-map"), value: "show", icon: "map" } ...(!isMultiselectMode()
? [
mode == "overview" || mode == "multi-select"
? {
text: $_("show-on-map"),
value: "show",
icon: "map",
}
: { : {
text: $_("show-in-overview"), text: $_("show-in-overview"),
value: "show", value: "show",
icon: "table-columns", icon: "table-columns",
}, },
]
{ text: $_("directions"), value: "direction", icon: "car" }, : []),
...(trail.gpx ...(!isMultiselectMode()
? [{ text: $_("directions"), value: "direction", icon: "car" }]
: []),
...(canExport()
? [ ? [
{ {
text: $_("export"), text: $_("export"),
@@ -63,28 +77,105 @@
}, },
] ]
: []), : []),
{ text: $_("print"), value: "print", icon: "print" }, ...(!isMultiselectMode()
...(isOwned ? [{ text: $_("print"), value: "print", icon: "print" }]
? [{ text: $_("add-to-list"), value: "list", icon: "bookmark" }]
: []), : []),
...(isOwned ...(!isFromCurrentUser()
? [{ text: $_("share"), value: "share", icon: "share" }] ? []
: []), : [
...(allowEdit {
text: $_("add-to-list"),
value: "list",
icon: "bookmark",
},
]),
...(isMultiselectMode() || !isFromCurrentUser()
? []
: [{ text: $_("share"), value: "share", icon: "share" }]),
...(allowEdit()
? [{ text: $_("edit"), value: "edit", icon: "pen" }] ? [{ text: $_("edit"), value: "edit", icon: "pen" }]
: []), : []),
...(isOwned ...(allowDelete()
? [{ text: $_("delete"), value: "delete", icon: "trash" }] ? [{ 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 }) { async function handleDropdownClick(item: { text: string; value: any }) {
if (!trail()) {
return;
}
const handle = handleFromRecordWithIRI(trail());
if (item.value == "show") { if (item.value == "show") {
if (hasTrail()) {
goto( goto(
mode == "overview" mode == "overview" || mode == "multi-select"
? `/map/trail/${handle}/${trail.id!}` ? `/map/trail/${handle}/${trailId()}`
: `/trail/view/${handle}/${trail.id!}`, : `/trail/view/${handle}/${trailId()}`,
); );
}
} else if (item.value == "list") { } else if (item.value == "list") {
lists = ( lists = (
await lists_index( await lists_index(
@@ -95,32 +186,54 @@
).items; ).items;
listSelectModal.openModal(); listSelectModal.openModal();
} else if (item.value == "direction") { } else if (item.value == "direction") {
if (hasTrail()) {
window window
.open( .open(
`https://www.google.com/maps/dir/Current+Location/${trail.lat},${trail.lon}`, `https://www.google.com/maps/dir/Current+Location/${trail()!.lat},${trail()!.lon}`,
"_blank", "_blank",
) )
?.focus(); ?.focus();
}
} else if (item.value == "print") { } 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") { } else if (item.value == "share") {
trailShareModal.openModal(); trailShareModal.openModal();
} else if (item.value == "download") { } else if (item.value == "download") {
trailExportModal.openModal(); trailExportModal.openModal();
} else if (item.value == "edit") { } else if (item.value == "edit") {
goto(`/trail/edit/${trail.id}`); if (hasTrail()) {
goto(`/trail/edit/${trailId()}`);
}
} else if (item.value == "delete") { } else if (item.value == "delete") {
confirmModal.openModal(); confirmModal.openModal();
} }
} }
async function exportTrail(exportSettings: { async function exportTrails(exportSettings: {
fileFormat: "gpx" | "json"; fileFormat: "gpx" | "json";
photos: boolean; photos: boolean;
summitLog: boolean; summitLog: boolean;
}) { }) {
if (trails !== undefined && trails.size > 0) {
for (const cTrail of trails) {
await doExportTrail(exportSettings, cTrail);
}
}
}
async function doExportTrail(
exportSettings: {
fileFormat: "gpx" | "json";
photos: boolean;
summitLog: boolean;
},
eTrail: Trail,
) {
try { try {
let fileData: string = await trail2gpx(trail, $currentUser); if (eTrail !== undefined) {
let fileData: string = await trail2gpx(eTrail, $currentUser);
if (exportSettings.fileFormat == "json") { if (exportSettings.fileFormat == "json") {
fileData = JSON.stringify( fileData = JSON.stringify(
gpx( gpx(
@@ -138,36 +251,40 @@
? "application/json" ? "application/json"
: "application/gpx+xml", : "application/gpx+xml",
}); });
saveAs(blob, `${trail.name}.${exportSettings.fileFormat}`); saveAs(blob, `${eTrail.name}.${exportSettings.fileFormat}`);
} else { } else {
const zip = new JSZip(); const zip = new JSZip();
zip.file( zip.file(
`${trail.name}.${exportSettings.fileFormat}`, `${eTrail.name}.${exportSettings.fileFormat}`,
fileData, fileData,
); );
if (exportSettings.photos) { if (exportSettings.photos) {
const photoFolder = zip.folder($_("photos")); const photoFolder = zip.folder($_("photos"));
for (const photo of trail.photos) { for (const photo of eTrail.photos) {
const photoURL = getFileURL(trail, photo); const photoURL = getFileURL(eTrail, photo);
const photoBlob = await fetch(photoURL).then( const photoBlob = await fetch(photoURL).then(
(response) => response.blob(), (response) => response.blob(),
); );
const photoData = new File([photoBlob], photo); const photoData = new File([photoBlob], photo);
photoFolder?.file(photo, photoData, { base64: true }); photoFolder?.file(photo, photoData, {
base64: true,
});
} }
} }
if (exportSettings.summitLog) { if (exportSettings.summitLog) {
let summitLogString = ""; let summitLogString = "";
for (const summitLog of trail.expand?.summit_logs_via_trail ?? []) { for (const summitLog of eTrail.expand
?.summit_logs_via_trail ?? []) {
summitLogString += `${summitLog.date},${summitLog.text}\n`; summitLogString += `${summitLog.date},${summitLog.text}\n`;
} }
zip.file( zip.file(
`${trail.name} - ${$_("summit-book")}.csv`, `${eTrail.name} - ${$_("summit-book")}.csv`,
summitLogString, summitLogString,
); );
} }
const blob = await zip.generateAsync({ type: "blob" }); const blob = await zip.generateAsync({ type: "blob" });
saveAs(blob, `${trail.name}.zip`); saveAs(blob, `${eTrail.name}.zip`);
}
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -179,28 +296,57 @@
} }
} }
async function deleteTrail() { async function deleteTrails() {
await trails_delete(trail); if (hasTrail()) {
setTimeout(() => { for (const dTrail of trails!) {
goto("/trails"); await doDeleteTrail(dTrail);
}, 500); }
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) { async function handleListSelection(list: List) {
try { try {
if (list.trails?.includes(trail.id!)) { let deleted = false;
await lists_remove_trail(list, trail); let multiple = false;
if (hasTrail()) {
multiple = true;
for (const lTrail of trails!) {
if (await doHandleListSelection(list, lTrail)) {
deleted = true;
}
}
}
if (deleted) {
show_toast({ show_toast({
type: "success", type: "success",
icon: "check", icon: "check",
text: `${$_("removed-trail-from")} "${list.name}"`, text: multiple
? `${$_("removed-trails-from")} "${list.name}"`
: `${$_("removed-trail-from")} "${list.name}"`,
}); });
} else { } else {
await lists_add_trail(list, trail);
show_toast({ show_toast({
type: "success", type: "success",
icon: "check", icon: "check",
text: `${$_("added-trail-to")} "${list.name}"`, text: multiple
? `${$_("added-trails-to")} "${list.name}"`
: `${$_("added-trail-to")} "${list.name}"`,
}); });
} }
} catch (e) { } catch (e) {
@@ -213,10 +359,51 @@
}); });
} }
} }
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> </script>
<Dropdown items={dropdownItems} onchange={(item) => handleDropdownClick(item)} <Dropdown items={dropdownItems()} onchange={(item) => handleDropdownClick(item)}
>{#snippet children({ toggleMenu: openDropdown })} >{#snippet children({ toggleMenu: openDropdown })}
{#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 <button
aria-label="Open dropdown" aria-label="Open dropdown"
class=" btn-primary !rounded-full h-12 w-12" class=" btn-primary !rounded-full h-12 w-12"
@@ -224,21 +411,27 @@
> >
<i class="fa fa-ellipsis-vertical"></i> <i class="fa fa-ellipsis-vertical"></i>
</button> </button>
{/if}
{/snippet} {/snippet}
</Dropdown> </Dropdown>
<ConfirmModal <ConfirmModal
text={$_("delete-trail-confirm")} text={$_("delete-trail-confirm")}
bind:this={confirmModal} bind:this={confirmModal}
onconfirm={deleteTrail} onconfirm={deleteTrails}
></ConfirmModal> ></ConfirmModal>
<ListSelectModal <ListSelectModal
{lists} {lists}
trails={getTrails()}
bind:this={listSelectModal} bind:this={listSelectModal}
onchange={(list) => handleListSelection(list)} onchange={(list) => handleListSelection(list)}
></ListSelectModal> ></ListSelectModal>
<TrailExportModal <TrailExportModal
bind:this={trailExportModal} bind:this={trailExportModal}
onexport={(settings) => exportTrail(settings)} onexport={(settings) => exportTrails(settings)}
></TrailExportModal> ></TrailExportModal>
<TrailShareModal {trail} bind:this={trailShareModal}></TrailShareModal> <TrailShareModal
trail={trail()}
onsave={handleShareUpdate}
bind:this={trailShareModal}
></TrailShareModal>

View File

@@ -401,7 +401,10 @@
{#if ($currentUser && $currentUser.actor == trail.author) || trail.expand?.trail_share_via_trail?.length || trail.public} {#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"> <div class="flex flex-col items-center gap-y-2">
<LikeButton {trail}></LikeButton> <LikeButton {trail}></LikeButton>
<TrailDropdown {trail} {mode} {handle}></TrailDropdown> <TrailDropdown
trails={new Set<Trail>([trail])}
{mode}
></TrailDropdown>
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -10,6 +10,7 @@
import SkeletonCard from "../base/skeleton_card.svelte"; import SkeletonCard from "../base/skeleton_card.svelte";
import SkeletonListItem from "../base/skeleton_list_item.svelte"; import SkeletonListItem from "../base/skeleton_list_item.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import TrailDropdown from "$lib/components/trail/trail_dropdown.svelte";
interface Props { interface Props {
filter?: TrailFilter | null; filter?: TrailFilter | null;
@@ -17,7 +18,10 @@
pagination?: { page: number; totalPages: number }; pagination?: { page: number; totalPages: number };
loading?: boolean; loading?: boolean;
fullWidthCards?: boolean; fullWidthCards?: boolean;
onupdate?: (filter: TrailFilter | null) => void; onupdate?: (
filter: TrailFilter | null,
selection: Set<Trail> | undefined,
) => void;
onpagination?: (page: number) => void; onpagination?: (page: number) => void;
} }
@@ -42,6 +46,9 @@
let selectedDisplayOption = $state(displayOptions[0].value); let selectedDisplayOption = $state(displayOptions[0].value);
let selection: Set<Trail> | undefined = $state();
let hoveredTrail: Trail | undefined = $state();
const sortOptions: SelectItem[] = [ const sortOptions: SelectItem[] = [
{ text: $_("name"), value: "name" }, { text: $_("name"), value: "name" },
{ text: $_("distance"), value: "distance" }, { text: $_("distance"), value: "distance" },
@@ -71,7 +78,7 @@
(storedSortOrder as typeof filter.sortOrder | null) ?? (storedSortOrder as typeof filter.sortOrder | null) ??
filter.sortOrder; filter.sortOrder;
} }
onupdate?.(filter); onupdate?.(filter, selection);
}); });
function setDisplayOption() { function setDisplayOption() {
@@ -83,7 +90,7 @@
return; return;
} }
localStorage.setItem("sort", filter.sort); localStorage.setItem("sort", filter.sort);
onupdate?.(filter); onupdate?.(filter, selection);
} }
function setSortOrder() { function setSortOrder() {
@@ -96,7 +103,7 @@
filter.sortOrder = "+"; filter.sortOrder = "+";
} }
localStorage.setItem("sort_order", filter.sortOrder); localStorage.setItem("sort_order", filter.sortOrder);
onupdate?.(filter); onupdate?.(filter, selection);
} }
function handleSortUpdate(sort: any) { function handleSortUpdate(sort: any) {
@@ -111,6 +118,96 @@
setSort(); 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> </script>
<div class="min-w-0"> <div class="min-w-0">
@@ -122,6 +219,15 @@
{onpagination} {onpagination}
></Pagination> ></Pagination>
</div> </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} {#if filter}
<div class="shrink-0"> <div class="shrink-0">
{#if selectedDisplayOption !== "table"} {#if selectedDisplayOption !== "table"}
@@ -158,7 +264,10 @@
<div id="trails" class="flex items-start flex-wrap gap-8 py-8 max-w-full"> <div id="trails" class="flex items-start flex-wrap gap-8 py-8 max-w-full">
{#if loading} {#if loading}
{#if selectedDisplayOption === "table"} {#if selectedDisplayOption === "table"}
<TrailTable trails={null} tableHeader={sortOptions} <TrailTable
trails={null}
selection={new Set<Trail>()}
tableHeader={sortOptions}
></TrailTable> ></TrailTable>
{:else} {:else}
{#each { length: 12 } as _, index} {#each { length: 12 } as _, index}
@@ -180,11 +289,13 @@
{#if selectedDisplayOption === "table"} {#if selectedDisplayOption === "table"}
<TrailTable <TrailTable
{trails} {trails}
{selection}
tableHeader={sortOptions.filter( tableHeader={sortOptions.filter(
(option) => option.value !== "elevation_loss", (option) => option.value !== "elevation_loss",
)} )}
{filter} {filter}
onsort={handleSortUpdate} onsort={handleSortUpdate}
onTrailSelect={(t) => handleSelectionUpdate(t)}
></TrailTable> ></TrailTable>
{:else} {:else}
{#each trails as trail} {#each trails as trail}
@@ -194,12 +305,26 @@
href="/trail/view/@{trail.author}{trail.domain href="/trail/view/@{trail.author}{trail.domain
? `@${trail.domain}` ? `@${trail.domain}`
: ''}/{trail.id}" : ''}/{trail.id}"
onmouseenter={(e) => handleMouseEnter(trail)}
onmouseleave={(e) => handleMouseLeave(trail)}
> >
{#if selectedDisplayOption === "cards"} {#if selectedDisplayOption === "cards"}
<TrailCard fullWidth={fullWidthCards} {trail} <TrailCard
fullWidth={fullWidthCards}
{trail}
selected={isSelected(trail)}
hovered={isHovered(trail)}
onTrailSelect={() =>
handleSelectionUpdate(trail)}
></TrailCard> ></TrailCard>
{:else} {:else}
<TrailListItem {trail}></TrailListItem> <TrailListItem
{trail}
selected={isSelected(trail)}
hovered={isHovered(trail)}
onTrailSelect={() =>
handleSelectionUpdate(trail)}
></TrailListItem>
{/if} {/if}
</a> </a>
{/each} {/each}

View File

@@ -19,9 +19,18 @@
interface Props { interface Props {
trail: Trail; trail: Trail;
showDescription?: boolean; 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( let thumbnail = $derived(
trail.photos.length trail.photos.length
@@ -35,6 +44,12 @@
: emptyStateTrailDark, : emptyStateTrailDark,
); );
function handleInputClick(e: Event) {
e.stopPropagation();
onTrailSelect?.();
hovered = true;
}
let expandedTags = $state(false); let expandedTags = $state(false);
function toggleExpandTags(e: MouseEvent) { function toggleExpandTags(e: MouseEvent) {
@@ -64,7 +79,7 @@
/> />
{/if} {/if}
</div> </div>
<div class="min-w-0 basis-full"> <div class="min-w-0 basis-full relative">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h4 class="font-semibold text-lg"> <h4 class="font-semibold text-lg">
{trail.name} {trail.name}
@@ -139,9 +154,6 @@
{#if trail.location} {#if trail.location}
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5> <h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
{/if} {/if}
<h5>
<i class="fa fa-gauge mr-3"></i>{$_(trail.difficulty ?? "?")}
</h5>
</div> </div>
<div class="flex flex-wrap mt-1 gap-x-4 gap-y-2 text-sm text-gray-500"> <div class="flex flex-wrap mt-1 gap-x-4 gap-y-2 text-sm text-gray-500">
@@ -168,10 +180,23 @@
</div> </div>
{#if showDescription} {#if showDescription}
<p <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 ?? "")} {formatHTMLAsText(trail.description ?? "")}
</p> </p>
{/if} {/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> </div>
</li> </li>

View File

@@ -16,7 +16,7 @@
import Select from "../base/select.svelte"; import Select from "../base/select.svelte";
interface Props { interface Props {
trail: Trail; trail?: Trail;
onsave?: () => void; onsave?: () => void;
} }
@@ -56,6 +56,10 @@
} }
async function shareTrail(item: SelectItem) { async function shareTrail(item: SelectItem) {
if (trail === undefined) {
return;
}
if (!item.value.isLocal && !trail.public) { if (!item.value.isLocal && !trail.public) {
displayShareError = true; displayShareError = true;
return; return;
@@ -80,6 +84,8 @@
} }
async function fetchShares() { async function fetchShares() {
if (trail === undefined) return;
sharesLoading = true; sharesLoading = true;
await trail_share_index({ trail: trail.id! }); await trail_share_index({ trail: trail.id! });
sharesLoading = false; sharesLoading = false;

View File

@@ -14,14 +14,18 @@
interface Props { interface Props {
tableHeader: SelectItem[]; tableHeader: SelectItem[];
trails?: Trail[] | null; trails?: Trail[] | null;
selection: Set<Trail> | undefined;
filter?: TrailFilter | null; 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 { function getColumnWidth(columnValue: string): string {
switch (columnValue) { switch (columnValue) {
case "select":
return "w-[2%]";
case "name": case "name":
return "w-[25%]"; return "w-[25%]";
case "distance": case "distance":
@@ -37,14 +41,68 @@
return ""; 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> </script>
<div <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"> <table class="w-full">
<thead> <thead>
<tr class="bg-secondary-hover"> <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} {#each tableHeader as column}
<th <th
class="p-4 text-left text-sm font-medium {getColumnWidth( class="p-4 text-left text-sm font-medium {getColumnWidth(
@@ -78,6 +136,16 @@
}/${trail.id}`, }/${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 <td
class="flex justify-between items-center text-sm relative" class="flex justify-between items-center text-sm relative"
> >

View File

@@ -14,6 +14,7 @@
"add-to-list": "Zu Liste hinzufügen", "add-to-list": "Zu Liste hinzufügen",
"add-waypoint": "Wegpunkt hinzufügen", "add-waypoint": "Wegpunkt hinzufügen",
"added-trail-to": "Route hinzugefügt zu", "added-trail-to": "Route hinzugefügt zu",
"added-trails-to": "Routen hinzugefügt zu",
"after": "Nach", "after": "Nach",
"all-activities": "Alle Aktivitäten", "all-activities": "Alle Aktivitäten",
"alphabetical": "Alphabetisch", "alphabetical": "Alphabetisch",
@@ -275,6 +276,7 @@
"register": "Registrieren", "register": "Registrieren",
"remote-users-cannot-edit": "Nutzer anderer Instanzen können nicht bearbeiten", "remote-users-cannot-edit": "Nutzer anderer Instanzen können nicht bearbeiten",
"removed-trail-from": "Route entfernt aus", "removed-trail-from": "Route entfernt aus",
"removed-trails-from": "Routen entfernt aus",
"required": "Pflichtfeld", "required": "Pflichtfeld",
"reset-password": "Passwort zurücksetzen", "reset-password": "Passwort zurücksetzen",
"road": "Straße", "road": "Straße",
@@ -289,6 +291,7 @@
"search-places": "Orte suchen", "search-places": "Orte suchen",
"search-trails": "Route suchen", "search-trails": "Route suchen",
"select-list": "Liste auswählen", "select-list": "Liste auswählen",
"selected": "selected",
"settings": "Einstellungen", "settings": "Einstellungen",
"settings-notification-comment-mention": "Jemand hat dich in einem Kommentar erwähnt", "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", "settings-notification-list-create": "Ein Benutzer, dem Du folgst, hat eine Liste erstellt",

View File

@@ -14,6 +14,7 @@
"add-to-list": "Add to list", "add-to-list": "Add to list",
"add-waypoint": "Add Waypoint", "add-waypoint": "Add Waypoint",
"added-trail-to": "Added trail to", "added-trail-to": "Added trail to",
"added-trails-to": "Added trails to",
"after": "After", "after": "After",
"all-activities": "All activities", "all-activities": "All activities",
"alphabetical": "Alphabetical", "alphabetical": "Alphabetical",
@@ -275,6 +276,7 @@
"register": "Register", "register": "Register",
"remote-users-cannot-edit": "", "remote-users-cannot-edit": "",
"removed-trail-from": "Removed trail from", "removed-trail-from": "Removed trail from",
"removed-trails-from": "Removed trails from",
"required": "Required", "required": "Required",
"reset-password": "Reset Password", "reset-password": "Reset Password",
"road": "Road", "road": "Road",
@@ -289,6 +291,7 @@
"search-places": "Search places", "search-places": "Search places",
"search-trails": "Search trails", "search-trails": "Search trails",
"select-list": "Select List", "select-list": "Select List",
"selected": "",
"settings": "Settings", "settings": "Settings",
"settings-notification-comment-mention": "Someone mentioned you in a comment", "settings-notification-comment-mention": "Someone mentioned you in a comment",
"settings-notification-list-create": "A user who you follow has created a list", "settings-notification-list-create": "A user who you follow has created a list",

View File

@@ -14,6 +14,7 @@
"add-to-list": "Añadir a la lista", "add-to-list": "Añadir a la lista",
"add-waypoint": "Añadir Punto de Interés", "add-waypoint": "Añadir Punto de Interés",
"added-trail-to": "Ruta añadida a", "added-trail-to": "Ruta añadida a",
"added-trails-to": "Rutas añadida a",
"after": "Después", "after": "Después",
"all-activities": "Todas las actividades", "all-activities": "Todas las actividades",
"alphabetical": "Alfabético", "alphabetical": "Alfabético",
@@ -275,6 +276,7 @@
"register": "Registrar", "register": "Registrar",
"remote-users-cannot-edit": "", "remote-users-cannot-edit": "",
"removed-trail-from": "Ruta borrada de", "removed-trail-from": "Ruta borrada de",
"removed-trails-from": "Rutas borrada de",
"required": "Obligatorio", "required": "Obligatorio",
"reset-password": "Restablecer Contraseña", "reset-password": "Restablecer Contraseña",
"road": "Carretera", "road": "Carretera",
@@ -289,6 +291,7 @@
"search-places": "Buscar lugares", "search-places": "Buscar lugares",
"search-trails": "Buscar ruta", "search-trails": "Buscar ruta",
"select-list": "Seleccionar Lista", "select-list": "Seleccionar Lista",
"selected": "",
"settings": "Configuración", "settings": "Configuración",
"settings-notification-comment-mention": "Someone mentioned you in a comment", "settings-notification-comment-mention": "Someone mentioned you in a comment",
"settings-notification-list-create": "Un usuario al que sigues ha creado una nueva lista", "settings-notification-list-create": "Un usuario al que sigues ha creado una nueva lista",

View File

@@ -14,6 +14,7 @@
"add-to-list": "Ajouter à une liste", "add-to-list": "Ajouter à une liste",
"add-waypoint": "Ajouter un point de passage", "add-waypoint": "Ajouter un point de passage",
"added-trail-to": "Ajouter un itinéraire à", "added-trail-to": "Ajouter un itinéraire à",
"added-trails-to": "Ajouter les itinéraires à",
"after": "Après", "after": "Après",
"all-activities": "Toutes les activités", "all-activities": "Toutes les activités",
"alphabetical": "Alphabétique", "alphabetical": "Alphabétique",
@@ -275,6 +276,7 @@
"register": "Créer un compte", "register": "Créer un compte",
"remote-users-cannot-edit": "", "remote-users-cannot-edit": "",
"removed-trail-from": "Enlever l'itinéraire de", "removed-trail-from": "Enlever l'itinéraire de",
"removed-trails-from": "Enlever les itinéraires de",
"required": "Requis", "required": "Requis",
"reset-password": "Réinitialiser le mot de passe", "reset-password": "Réinitialiser le mot de passe",
"road": "Road", "road": "Road",
@@ -289,6 +291,7 @@
"search-places": "Chercher des lieux", "search-places": "Chercher des lieux",
"search-trails": "Chercher un itinéraire", "search-trails": "Chercher un itinéraire",
"select-list": "Liste de choix", "select-list": "Liste de choix",
"selected": "",
"settings": "Paramètres", "settings": "Paramètres",
"settings-notification-comment-mention": "Someone mentioned you in a comment", "settings-notification-comment-mention": "Someone mentioned you in a comment",
"settings-notification-list-create": "Un utilisateur que vous suivez à créé une nouvelle liste", "settings-notification-list-create": "Un utilisateur que vous suivez à créé une nouvelle liste",

View File

@@ -14,6 +14,7 @@
"add-to-list": "Hozzáadás a listához", "add-to-list": "Hozzáadás a listához",
"add-waypoint": "Útvonalpont hozzáadása", "add-waypoint": "Útvonalpont hozzáadása",
"added-trail-to": "Hozzáadott nyomvonal a", "added-trail-to": "Hozzáadott nyomvonal a",
"added-trails-to": "Hozzáadott nyomvonalak a",
"after": "After", "after": "After",
"all-activities": "All activities", "all-activities": "All activities",
"alphabetical": "Betűrendben", "alphabetical": "Betűrendben",
@@ -275,6 +276,7 @@
"register": "Regisztráció", "register": "Regisztráció",
"remote-users-cannot-edit": "", "remote-users-cannot-edit": "",
"removed-trail-from": "Eltávolított nyomvonal a", "removed-trail-from": "Eltávolított nyomvonal a",
"removed-trails-from": "Eltávolított nyomvonalak a",
"required": "Kötelező", "required": "Kötelező",
"reset-password": "Reset Password", "reset-password": "Reset Password",
"road": "Road", "road": "Road",
@@ -289,6 +291,7 @@
"search-places": "Search places", "search-places": "Search places",
"search-trails": "Nyomvonalak keresése", "search-trails": "Nyomvonalak keresése",
"select-list": "Lista kiválasztása", "select-list": "Lista kiválasztása",
"selected": "",
"settings": "Beállítások", "settings": "Beállítások",
"settings-notification-comment-mention": "Someone mentioned you in a comment", "settings-notification-comment-mention": "Someone mentioned you in a comment",
"settings-notification-list-create": "A user who you follow has created a list", "settings-notification-list-create": "A user who you follow has created a list",

View File

@@ -14,6 +14,7 @@
"add-to-list": "Aggiungi alla lista", "add-to-list": "Aggiungi alla lista",
"add-waypoint": "Aggiungi un punto di passaggio", "add-waypoint": "Aggiungi un punto di passaggio",
"added-trail-to": "Percorso aggiunto a", "added-trail-to": "Percorso aggiunto a",
"added-trails-to": "Percorsi aggiunto a",
"after": "Dopo", "after": "Dopo",
"all-activities": "Tutte le Attività", "all-activities": "Tutte le Attività",
"alphabetical": "Alfabetico", "alphabetical": "Alfabetico",
@@ -275,6 +276,7 @@
"register": "Registrati", "register": "Registrati",
"remote-users-cannot-edit": "", "remote-users-cannot-edit": "",
"removed-trail-from": "Percorso rimosso da", "removed-trail-from": "Percorso rimosso da",
"removed-trails-from": "Percorsi rimosso da",
"required": "Obbligatorio", "required": "Obbligatorio",
"reset-password": "Ripristinare Password", "reset-password": "Ripristinare Password",
"road": "Road", "road": "Road",
@@ -289,6 +291,7 @@
"search-places": "Search places", "search-places": "Search places",
"search-trails": "Cerca percorsi", "search-trails": "Cerca percorsi",
"select-list": "Seleziona lista", "select-list": "Seleziona lista",
"selected": "",
"settings": "Impostazioni", "settings": "Impostazioni",
"settings-notification-comment-mention": "Someone mentioned you in a comment", "settings-notification-comment-mention": "Someone mentioned you in a comment",
"settings-notification-list-create": "Un utente che segui ha creato una lista", "settings-notification-list-create": "Un utente che segui ha creato una lista",

View File

@@ -14,6 +14,7 @@
"add-to-list": "Toevoegen aan lijst", "add-to-list": "Toevoegen aan lijst",
"add-waypoint": "Routepunt toevoegen", "add-waypoint": "Routepunt toevoegen",
"added-trail-to": "Route toegevoegd aan", "added-trail-to": "Route toegevoegd aan",
"added-trails-to": "Route toegevoegd aan",
"after": "Na", "after": "Na",
"all-activities": "Alle activiteiten", "all-activities": "Alle activiteiten",
"alphabetical": "Alfabetisch", "alphabetical": "Alfabetisch",
@@ -275,6 +276,7 @@
"register": "Registreren", "register": "Registreren",
"remote-users-cannot-edit": "", "remote-users-cannot-edit": "",
"removed-trail-from": "Route verwijderd van", "removed-trail-from": "Route verwijderd van",
"removed-trails-from": "Routes verwijderd van",
"required": "Verplicht", "required": "Verplicht",
"reset-password": "Wachtwoord opnieuw instellen", "reset-password": "Wachtwoord opnieuw instellen",
"road": "Weg", "road": "Weg",
@@ -289,6 +291,7 @@
"search-places": "Zoek plaatsen", "search-places": "Zoek plaatsen",
"search-trails": "Zoek routes", "search-trails": "Zoek routes",
"select-list": "Kies een lijst", "select-list": "Kies een lijst",
"selected": "",
"settings": "Instellingen", "settings": "Instellingen",
"settings-notification-comment-mention": "Someone mentioned you in a comment", "settings-notification-comment-mention": "Someone mentioned you in a comment",
"settings-notification-list-create": "Een gebruiker die je volgt, heeft een lijst gecreëerd", "settings-notification-list-create": "Een gebruiker die je volgt, heeft een lijst gecreëerd",

View File

@@ -14,6 +14,7 @@
"add-to-list": "Dodaj do listy", "add-to-list": "Dodaj do listy",
"add-waypoint": "Dodaj Punkt", "add-waypoint": "Dodaj Punkt",
"added-trail-to": "Dodaj szlak do", "added-trail-to": "Dodaj szlak do",
"added-trails-to": "Dodaj szlaki do",
"after": "Po", "after": "Po",
"all-activities": "Wszystkie aktywności", "all-activities": "Wszystkie aktywności",
"alphabetical": "Alfabetyczne", "alphabetical": "Alfabetyczne",
@@ -275,6 +276,7 @@
"register": "Zarejestruj", "register": "Zarejestruj",
"remote-users-cannot-edit": "", "remote-users-cannot-edit": "",
"removed-trail-from": "Usunięto szlak z", "removed-trail-from": "Usunięto szlak z",
"removed-trails-from": "Usunięto szlaki z",
"required": "Wymagane", "required": "Wymagane",
"reset-password": "Resetuj hasło", "reset-password": "Resetuj hasło",
"road": "Road", "road": "Road",
@@ -289,6 +291,7 @@
"search-places": "Szukaj miejsc", "search-places": "Szukaj miejsc",
"search-trails": "Szukaj szlaków", "search-trails": "Szukaj szlaków",
"select-list": "Wybierz Listę", "select-list": "Wybierz Listę",
"selected": "",
"settings": "Ustawienia", "settings": "Ustawienia",
"settings-notification-comment-mention": "Someone mentioned you in a comment", "settings-notification-comment-mention": "Someone mentioned you in a comment",
"settings-notification-list-create": "Użytkownik, którego obserwujesz, utworzył listę", "settings-notification-list-create": "Użytkownik, którego obserwujesz, utworzył listę",

View File

@@ -14,6 +14,7 @@
"add-to-list": "Adicionar à lista", "add-to-list": "Adicionar à lista",
"add-waypoint": "Adicionar ponto de vista", "add-waypoint": "Adicionar ponto de vista",
"added-trail-to": "Trilha adicionada para", "added-trail-to": "Trilha adicionada para",
"added-trails-to": "trilhas adicionada para",
"after": "Depois", "after": "Depois",
"all-activities": "Todas as atividades", "all-activities": "Todas as atividades",
"alphabetical": "Alfabético", "alphabetical": "Alfabético",
@@ -275,6 +276,7 @@
"register": "Registo", "register": "Registo",
"remote-users-cannot-edit": "", "remote-users-cannot-edit": "",
"removed-trail-from": "Trilha removida de", "removed-trail-from": "Trilha removida de",
"removed-trails-from": "Trilhos removidos de",
"required": "Obrigatório", "required": "Obrigatório",
"reset-password": "Reset Password", "reset-password": "Reset Password",
"road": "Road", "road": "Road",
@@ -289,6 +291,7 @@
"search-places": "Search places", "search-places": "Search places",
"search-trails": "Procurar trilhos", "search-trails": "Procurar trilhos",
"select-list": "Selecionar lista", "select-list": "Selecionar lista",
"selected": "",
"settings": "Definições", "settings": "Definições",
"settings-notification-comment-mention": "Someone mentioned you in a comment", "settings-notification-comment-mention": "Someone mentioned you in a comment",
"settings-notification-list-create": "A user who you follow has created a list", "settings-notification-list-create": "A user who you follow has created a list",

View File

@@ -14,6 +14,7 @@
"add-to-list": "添加到列表", "add-to-list": "添加到列表",
"add-waypoint": "添加坐标", "add-waypoint": "添加坐标",
"added-trail-to": "添加路线到", "added-trail-to": "添加路线到",
"added-trails-to": "添加路线到",
"after": "之后", "after": "之后",
"all-activities": "所有活动", "all-activities": "所有活动",
"alphabetical": "字母", "alphabetical": "字母",
@@ -275,6 +276,7 @@
"register": "注册", "register": "注册",
"remote-users-cannot-edit": "", "remote-users-cannot-edit": "",
"removed-trail-from": "路线已删除自", "removed-trail-from": "路线已删除自",
"removed-trails-from": "路线已删除自",
"required": "必填", "required": "必填",
"reset-password": "重置密码", "reset-password": "重置密码",
"road": "Road", "road": "Road",
@@ -289,6 +291,7 @@
"search-places": "Search places", "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-comment-mention": "Someone mentioned you in a comment",
"settings-notification-list-create": "A user who you follow has created a list", "settings-notification-list-create": "A user who you follow has created a list",

View File

@@ -14,6 +14,8 @@ import JSZip from "jszip";
import type { AuthRecord } from "pocketbase"; import type { AuthRecord } from "pocketbase";
import * as xmldom from 'xmldom'; import * as xmldom from 'xmldom';
import { bbox, splitMultiLineStringToLineStrings } from "./geojson_util"; 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) { 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) { export async function trail2gpx(trail: Trail, user?: AuthRecord) {
let gpxTrail = trail;
if (!trail.expand?.gpx_data) { if (!trail.expand?.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") 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) { if (gpx instanceof Error) {
throw gpx; throw gpx;
@@ -92,7 +105,7 @@ export async function trail2gpx(trail: Trail, user?: AuthRecord) {
gpx.wpt = []; 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) const gpxWpt = gpx.wpt.find((w) => w.$.lat == wp.lat && w.$.lon == wp.lon)
if (!gpxWpt) { if (!gpxWpt) {
gpx.wpt.push({ gpx.wpt.push({