From 90cba5ac599164cf0b0799afad7b38ee12d95d6d Mon Sep 17 00:00:00 2001 From: slothful-vassal Date: Fri, 18 Apr 2025 19:17:43 +0200 Subject: [PATCH 01/10] trail table: multi select --- .../components/trail/trail_dropdown.svelte | 208 +++++++++++++----- .../lib/components/trail/trail_list.svelte | 79 ++++++- .../components/trail/trail_share_modal.svelte | 6 +- .../lib/components/trail/trail_table.svelte | 79 ++++++- 4 files changed, 307 insertions(+), 65 deletions(-) diff --git a/web/src/lib/components/trail/trail_dropdown.svelte b/web/src/lib/components/trail/trail_dropdown.svelte index d2aef054..eee2c019 100644 --- a/web/src/lib/components/trail/trail_dropdown.svelte +++ b/web/src/lib/components/trail/trail_dropdown.svelte @@ -23,11 +23,13 @@ import { pb } from "$lib/pocketbase"; interface Props { - trail: Trail; - mode: "overview" | "map" | "list"; + trail?: Trail; + trails?: Set | undefined; + mode: "overview" | "map" | "list" | "trails"; + onconfirm?: () => void; } - let { trail, mode }: Props = $props(); + let { trail, trails, mode, onconfirm }: Props = $props(); let confirmModal: ConfirmModal; let listSelectModal: ListSelectModal; @@ -36,23 +38,33 @@ let lists: List[] = $state([]); - const allowEdit = + const allowEdit = + trails === undefined && + trail !== undefined && ( trail.author == $currentUser?.id || 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", - }, - - { text: $_("directions"), value: "direction", icon: "car" }, - ...(trail.gpx + ...(mode != "trails" + ? [ + mode == "overview" + ? { text: $_("show-on-map"), value: "show", icon: "map" } + : { + text: $_("show-in-overview"), + value: "show", + icon: "table-columns", + }, + ] + : [] + ), + ...(mode != "trails" + ? [ + { text: $_("directions"), value: "direction", icon: "car" } + ] : [] + ), + ...(mode != "trails" && trail !== undefined && trail.gpx ? [ { text: $_("export"), @@ -61,28 +73,53 @@ }, ] : []), - { text: $_("print"), value: "print", icon: "print" }, - ...(trail.author != pb.authStore.record?.id + ...(mode != "trails" + ? [ + { text: $_("print"), value: "print", icon: "print" }, + ] + : []), + ...(trail !== undefined && trail.author != pb.authStore.record?.id ? [] : [{ text: $_("add-to-list"), value: "list", icon: "bookmark" }]), - ...(trail.author != pb.authStore.record?.id + ...(mode == "trails" || (trail !== undefined && trail.author != pb.authStore.record?.id) ? [] : [{ text: $_("share"), value: "share", icon: "share" }]), ...(allowEdit ? [{ text: $_("edit"), value: "edit", icon: "pen" }] : []), - ...(trail.author == $currentUser?.id + ...(allowDelete() ? [{ text: $_("delete"), value: "delete", icon: "trash" }] : []), ]; + + function allowDelete(): boolean { + if (mode === "trails") { + return true; + } + else { + return allowDeleteTrail(trail); + } + } + + function allowDeleteTrail(dTrail?: Trail): boolean { + if (dTrail !== undefined) { + return dTrail.author == $currentUser?.id; + } + else { + return false; + } + } + async function handleDropdownClick(item: { text: string; value: any }) { if (item.value == "show") { - goto( - mode == "overview" - ? `/map/trail/${trail.id!}` - : `/trail/view/${trail.id!}`, - ); + if (trail !== undefined) { + goto( + mode == "overview" + ? `/map/trail/${trail.id!}` + : `/trail/view/${trail.id!}`, + ); + } } else if (item.value == "list") { lists = ( await lists_index( @@ -93,32 +130,53 @@ ).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 (trail !== undefined) { + window + .open( + `https://www.google.com/maps/dir/Current+Location/${trail.lat},${trail.lon}`, + "_blank", + ) + ?.focus(); + } } else if (item.value == "print") { - goto(`/map/trail/${trail.id}/print`); + if (trail !== undefined) { + goto(`/map/trail/${trail.id}/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 (trail !== undefined) { + goto(`/trail/edit/${trail.id}`); + } } else if (item.value == "delete") { confirmModal.openModal(); } } - async function exportTrail(exportSettings: { + async function exportTrails(exportSettings: { fileFormat: "gpx" | "json"; photos: boolean; summitLog: boolean; }) { + if (trails !== undefined && trails.size > 0) { + for (const cTrail of trails) { + await doExportTrail(exportSettings, cTrail); + } + } + else if (trail !== undefined) { + await doExportTrail(exportSettings, trail); + } + } + + async function doExportTrail(exportSettings: { + fileFormat: "gpx" | "json"; + photos: boolean; + summitLog: boolean; + }, eTrail: Trail) { try { - let fileData: string = await trail2gpx(trail); + let fileData: string = await trail2gpx(eTrail); if (exportSettings.fileFormat == "json") { fileData = JSON.stringify( gpx( @@ -136,17 +194,17 @@ ? "application/json" : "application/gpx+xml", }); - saveAs(blob, `${trail.name}.${exportSettings.fileFormat}`); + saveAs(blob, `${eTrail.name}.${exportSettings.fileFormat}`); } else { const zip = new JSZip(); zip.file( - `${trail.name}.${exportSettings.fileFormat}`, + `${eTrail.name}.${exportSettings.fileFormat}`, fileData, ); if (exportSettings.photos) { const photoFolder = zip.folder($_("photos")); - for (const photo of trail.photos) { - const photoURL = getFileURL(trail, photo); + for (const photo of eTrail.photos) { + const photoURL = getFileURL(eTrail, photo); const photoBlob = await fetch(photoURL).then( (response) => response.blob(), ); @@ -156,16 +214,16 @@ } if (exportSettings.summitLog) { let summitLogString = ""; - for (const summitLog of trail.expand?.summit_logs ?? []) { + for (const summitLog of eTrail.expand?.summit_logs ?? []) { summitLogString += `${summitLog.date},${summitLog.text}\n`; } zip.file( - `${trail.name} - ${$_("summit-book")}.csv`, + `${eTrail.name} - ${$_("summit-book")}.csv`, summitLogString, ); } const blob = await zip.generateAsync({ type: "blob" }); - saveAs(blob, `${trail.name}.zip`); + saveAs(blob, `${eTrail.name}.zip`); } } catch (e) { console.error(e); @@ -177,28 +235,59 @@ } } - async function deleteTrail() { - await trails_delete(trail); - setTimeout(() => { - goto("/trails"); - }, 500); + async function deleteTrails() { + if (trails !== undefined && trails.size > 0) { + for (const dTrail of trails) { + await doDeleteTrail(dTrail); + } + + onconfirm?.(); + } + else if (trail !== undefined) { + await doDeleteTrail(trail); + + setTimeout(() => { + goto("/trails"); + }, 500); + } + } + + async function doDeleteTrail(dTrail: Trail) { + if (!allowDeleteTrail(dTrail)) return; + + await trails_delete(dTrail); } 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 (trails !== undefined && trails.size > 0) { + multiple = true; + for (const lTrail of trails) { + if (await doHandleListSelection(list, lTrail)) { + deleted = true; + } + } + } + else if (trail !== undefined) + { + deleted = await doHandleListSelection(list, trail); + } + + 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); + } + else { 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) { @@ -211,6 +300,17 @@ }); } } + + async function doHandleListSelection(list: List, lTrail: Trail): Promise { + if (list.trails?.includes(lTrail.id!)) { + await lists_remove_trail(list, lTrail); + return true; + } else { + await lists_add_trail(list, lTrail); + } + + return false; + } handleDropdownClick(item)} @@ -228,7 +328,7 @@ exportTrail(settings)} + onexport={(settings) => exportTrails(settings)} > diff --git a/web/src/lib/components/trail/trail_list.svelte b/web/src/lib/components/trail/trail_list.svelte index 888ae014..071cbc75 100644 --- a/web/src/lib/components/trail/trail_list.svelte +++ b/web/src/lib/components/trail/trail_list.svelte @@ -10,14 +10,15 @@ 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; trails: Trail[]; pagination?: { page: number; totalPages: number }; loading?: boolean; fullWidthCards?: boolean; - onupdate?: (filter: TrailFilter | null) => void; + onupdate?: (filter: TrailFilter | null, selection: Set | undefined) => void; onpagination?: (page: number) => void; } @@ -42,6 +43,8 @@ let selectedDisplayOption = $state(displayOptions[0].value); + let selection: Set | undefined = $state(); + const sortOptions: SelectItem[] = [ { text: $_("name"), value: "name" }, { text: $_("distance"), value: "distance" }, @@ -70,7 +73,7 @@ (storedSortOrder as typeof filter.sortOrder | null) ?? filter.sortOrder; } - onupdate?.(filter); + onupdate?.(filter, selection); }); function setDisplayOption() { @@ -82,7 +85,7 @@ return; } localStorage.setItem("sort", filter.sort); - onupdate?.(filter); + onupdate?.(filter, selection); } function setSortOrder() { @@ -95,7 +98,7 @@ filter.sortOrder = "+"; } localStorage.setItem("sort_order", filter.sortOrder); - onupdate?.(filter); + onupdate?.(filter, selection); } function handleSortUpdate(sort: any) { @@ -110,6 +113,64 @@ setSort(); } } + + function handleSelectionUpdate(sTrail: Trail) { + if (sTrail === undefined) { + let addTrails = false; + if (selection === undefined) { + selection = new Set(); + addTrails = true; + } + else { + addTrails = selection.size === 0 || selection.size !== trails.length; + selection.clear(); + } + + if (addTrails) { + trails.forEach((value: Trail) => selectTrail(value)); + } + } + else { + selectTrail(sTrail); + } + + onupdate?.(filter, selection); + } + + function selectTrail(trail: Trail) { + if (trail !== undefined) { + if (selection === undefined) selection = new Set(); + + let exists = false; + for (const sTrail of selection) { + if (sTrail !== undefined && sTrail.id === trail.id) { + exists = true; + break; + } + } + + if (exists) { + let newSelection = new Set(); + for (const sTrail of selection) { + if (sTrail !== undefined && sTrail.id === trail.id) + continue; + + newSelection.add(sTrail); + } + + selection = newSelection; + } + else { + selection.add(trail); + } + } + } + + function handleTrailsEditDone() { + setTimeout(() => { + onupdate?.(filter, selection); + }, 500); + }
@@ -140,6 +201,8 @@ >
+ {:else if selection !== undefined && selection.size > 0} + {/if} {/if} @@ -157,7 +220,7 @@
{#if loading} {#if selectedDisplayOption === "table"} - } tableHeader={sortOptions} > {:else} {#each { length: 12 } as _, index} @@ -179,11 +242,13 @@ {#if selectedDisplayOption === "table"} option.value !== "elevation_loss", )} {filter} onsort={handleSortUpdate} + onselect={handleSelectionUpdate} > {:else} {#each trails as trail} @@ -217,4 +282,4 @@ :global(.rotated) { transform: rotate(180deg); } - + \ No newline at end of file diff --git a/web/src/lib/components/trail/trail_share_modal.svelte b/web/src/lib/components/trail/trail_share_modal.svelte index 54c00074..082ecc25 100644 --- a/web/src/lib/components/trail/trail_share_modal.svelte +++ b/web/src/lib/components/trail/trail_share_modal.svelte @@ -17,7 +17,7 @@ import UserSearch from "../user_search.svelte"; interface Props { - trail: Trail; + trail?: Trail; onsave?: () => void; } @@ -55,6 +55,8 @@ } async function shareTrail(item: SelectItem) { + if (trail === undefined) return; + const share = new TrailShare(item.value.id, trail.id!, "view"); await trail_share_create(share); fetchShares(); @@ -74,6 +76,8 @@ } async function fetchShares() { + if (trail === undefined) return; + sharesLoading = true; await trail_share_index({ trail: trail.id! }); sharesLoading = false; diff --git a/web/src/lib/components/trail/trail_table.svelte b/web/src/lib/components/trail/trail_table.svelte index 795bc9c8..d2fa6b6d 100644 --- a/web/src/lib/components/trail/trail_table.svelte +++ b/web/src/lib/components/trail/trail_table.svelte @@ -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 | undefined; filter?: TrailFilter | null; onsort?: (value: any) => void + onselect?: (value: any) => void } - let { tableHeader, trails = null, filter = null, onsort }: Props = $props(); + let { tableHeader, trails = null, selection, filter = null, onsort, onselect }: Props = $props(); function getColumnWidth(columnValue: string): string { switch (columnValue) { + case "select": + return "w-[2%]"; case "name": return "w-[25%]"; case "distance": @@ -37,6 +41,51 @@ 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 viewTrail(trail: Trail) { + goto(`/trail/view/${trail.id}`) + } + + 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; + } +
+ +
+ +
+ {#each tableHeader as column} goto(`/trail/view/${trail.id}`)} + onclick={() => viewTrail(trail)} > + +
+ setSelectedTrail(e, trail)} + /> +
+ From 1b096ce71aa156885d683b85fdb21ac3cc0c9a16 Mon Sep 17 00:00:00 2001 From: slothful-vassal Date: Mon, 28 Apr 2025 20:07:49 +0200 Subject: [PATCH 02/10] trail cards: multi select --- .../lib/components/trail/trail_card.svelte | 33 ++++++++- .../lib/components/trail/trail_list.svelte | 67 +++++++++++++++++-- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/web/src/lib/components/trail/trail_card.svelte b/web/src/lib/components/trail/trail_card.svelte index 4bb31756..f5ada619 100644 --- a/web/src/lib/components/trail/trail_card.svelte +++ b/web/src/lib/components/trail/trail_card.svelte @@ -1,10 +1,12 @@
{/if}
- {#if (trail.public || trailIsShared) && $currentUser} + {#if hovered || selected} +
+ handleInputClick(e)} + /> +
+ {/if} + {#if (trail.public || trailIsShared) && pb.authStore.record}
- {#if trail.public && $currentUser} + {#if trail.public && pb.authStore.record} | undefined = $state(); + let hoveredTrail: Trail | undefined = $state(); const sortOptions: SelectItem[] = [ { text: $_("name"), value: "name" }, @@ -114,6 +115,39 @@ } } + 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(sTrail: Trail) { if (sTrail === undefined) { let addTrails = false; @@ -134,7 +168,16 @@ selectTrail(sTrail); } - onupdate?.(filter, selection); + //onupdate?.(filter, selection); + } + + function handleHoverUpdate(hTrail: Trail) { + if (hTrail === undefined) { + return; + } + + if (hoveredTrail === undefined) hoveredTrail = hTrail; + else hoveredTrail = undefined; } function selectTrail(trail: Trail) { @@ -171,6 +214,17 @@ onupdate?.(filter, selection); }, 500); } + + function handleMouseEnter(trail: Trail) { + handleHoverUpdate(trail); + } + function handleMouseLeave(trail: Trail) { + handleHoverUpdate(trail); + } + function handeTrailSelect(trail: Trail) { + handleSelectionUpdate(trail); + //hoveredTrail = undefined; + }
@@ -183,7 +237,11 @@ >
{#if filter} + {#if selection !== undefined && selection.size > 0} + + {/if}
+ {#if selectedDisplayOption !== "table"}

{$_("sort")}

@@ -201,8 +259,6 @@ >
- {:else if selection !== undefined && selection.size > 0} - {/if}
{/if} @@ -258,7 +314,10 @@ href="/trail/view/{trail.id}" > {#if selectedDisplayOption === "cards"} - handleMouseEnter(trail)} onmouseleave={(e) => handleMouseLeave(trail)} + onTrailSelect={() => handeTrailSelect(trail)} > {:else} From 443dcd9ed7ea5031e2b74665630af51bdfe78557 Mon Sep 17 00:00:00 2001 From: brotkrume Date: Wed, 7 May 2025 18:37:20 +0200 Subject: [PATCH 03/10] multiselect for list view, several multiselect fixes, code beautify --- .gitignore | 5 +- .../lib/components/trail/trail_card.svelte | 6 +- .../components/trail/trail_dropdown.svelte | 295 +++++++++--------- .../components/trail/trail_info_panel.svelte | 2 +- .../lib/components/trail/trail_list.svelte | 97 +++--- .../components/trail/trail_list_item.svelte | 210 +++++++------ .../components/trail/trail_share_modal.svelte | 2 +- .../lib/components/trail/trail_table.svelte | 5 +- 8 files changed, 324 insertions(+), 298 deletions(-) diff --git a/.gitignore b/.gitignore index 384585ed..61487c08 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,12 @@ db/pocketbase* -search/meilisearch +search/meilisearch* search/data.ms* search/dumps run.sh build*.sh +start.* -data/ \ No newline at end of file +data*/ diff --git a/web/src/lib/components/trail/trail_card.svelte b/web/src/lib/components/trail/trail_card.svelte index b4797b97..fa80a20e 100644 --- a/web/src/lib/components/trail/trail_card.svelte +++ b/web/src/lib/components/trail/trail_card.svelte @@ -2,7 +2,7 @@ import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg"; import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg"; import type { Trail } from "$lib/models/trail"; - import { pb } from "$lib/pocketbase"; + import { currentUser } from "$lib/stores/user_store"; import { theme } from "$lib/stores/theme_store"; import { getFileURL, isVideoURL } from "$lib/util/file_util"; import { @@ -96,13 +96,13 @@ />
{/if} - {#if (trail.public || trailIsShared) && pb.authStore.record} + {#if (trail.public || trailIsShared) && $currentUser}
- {#if trail.public && pb.authStore.record} + {#if trail.public && $currentUser} | undefined; - mode: "overview" | "map" | "list" | "trails"; + mode: "overview" | "map" | "list"; onconfirm?: () => void; } - let { trail, trails, mode, onconfirm }: Props = $props(); + let { trails, mode, onconfirm }: Props = $props(); let confirmModal: ConfirmModal; let listSelectModal: ListSelectModal; @@ -37,86 +37,113 @@ let lists: List[] = $state([]); - const allowEdit = - trails === undefined && - trail !== undefined && ( - trail.author == $currentUser?.id || - trail.expand?.trail_share_via_trail?.some( - (s) => s.permission == "edit", - )); + function allowEdit() : boolean { + return hasTrail() && !isMultiselectMode() && + ( + trail()!.author === $currentUser?.id || + trail()!.expand?.trail_share_via_trail?.some( + (s) => s.permission == "edit", + ) + )!; + } - const dropdownItems: DropdownItem[] = [ - ...(mode != "trails" - ? [ - mode == "overview" - ? { text: $_("show-on-map"), value: "show", icon: "map" } - : { - text: $_("show-in-overview"), - value: "show", - icon: "table-columns", - }, - ] - : [] - ), - ...(mode != "trails" - ? [ - { text: $_("directions"), value: "direction", icon: "car" } - ] : [] - ), - ...(mode != "trails" && trail !== undefined && trail.gpx - ? [ - { - text: $_("export"), - value: "download", - icon: "download", - }, - ] - : []), - ...(mode != "trails" - ? [ - { text: $_("print"), value: "print", icon: "print" }, - ] - : []), - ...(trail !== undefined && trail.author != $currentUser?.id - ? [] - : [{ text: $_("add-to-list"), value: "list", icon: "bookmark" }]), - ...(mode == "trails" || (trail !== undefined && trail.author != $currentUser?.id) - ? [] - : [{ text: $_("share"), value: "share", icon: "share" }]), - ...(allowEdit - ? [{ text: $_("edit"), value: "edit", icon: "pen" }] - : []), - ...(allowDelete() - ? [{ 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 canExport() : boolean { + return !isMultiselectMode() && hasTrail() && trail()!.expand?.gpx_data != undefined && trail()!.expand!.gpx_data!.length > 0; + } + + function trailId() : string | undefined { + return trail()?.id; + } + function trail() : Trail | undefined { + return hasTrail() ? [...trails!][0] : undefined; + } - function allowDelete(): boolean { - if (mode === "trails") { - return true; - } - else { - return allowDeleteTrail(trail); + function dropdownItems(): DropdownItem[] { + return [ + ...(!isMultiselectMode() + ? [ + mode == "overview" + ? { 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" }] + : []), + ]; + } + + function isFromCurrentUser(uTrail?: Trail) : boolean { + if (uTrail !== undefined) { + return uTrail.author === $currentUser?.id; + } else if (trails !== undefined && trails.size > 0) { + for (const sTrail of trails) { + if (sTrail.author === $currentUser?.id){ + return true; + } + } } + + return false; + } + + function allowDelete(): boolean { + return isFromCurrentUser(); } function allowDeleteTrail(dTrail?: Trail): boolean { - if (dTrail !== undefined) { - return dTrail.author == $currentUser?.id; - } - else { - return false; - } + return isFromCurrentUser(dTrail); } async function handleDropdownClick(item: { text: string; value: any }) { if (item.value == "show") { - if (trail !== undefined) { + if (hasTrail()) { goto( mode == "overview" - ? `/map/trail/${trail.id!}` - : `/trail/view/${trail.id!}`, + ? `/map/trail/${trailId()}` + : `/trail/view/${trailId()}`, ); } } else if (item.value == "list") { @@ -129,25 +156,25 @@ ).items; listSelectModal.openModal(); } else if (item.value == "direction") { - if (trail !== undefined) { + if (hasTrail()) { window .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", ) ?.focus(); } } else if (item.value == "print") { - if (trail !== undefined) { - goto(`/map/trail/${trail.id}/print`); + if (hasTrail()) { + goto(`/map/trail/${trailId()}/print`); } } else if (item.value == "share") { trailShareModal.openModal(); } else if (item.value == "download") { trailExportModal.openModal(); } else if (item.value == "edit") { - if (trail !== undefined) { - goto(`/trail/edit/${trail.id}`); + if (hasTrail()) { + goto(`/trail/edit/${trailId()}`); } } else if (item.value == "delete") { confirmModal.openModal(); @@ -164,9 +191,6 @@ await doExportTrail(exportSettings, cTrail); } } - else if (trail !== undefined) { - await doExportTrail(exportSettings, trail); - } } async function doExportTrail(exportSettings: { @@ -175,54 +199,56 @@ summitLog: boolean; }, eTrail: Trail) { 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 (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, + ), ), - ), - ); - } - 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 ?? []) { - summitLogString += `${summitLog.date},${summitLog.text}\n`; - } + 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} - ${$_("summit-book")}.csv`, - summitLogString, + `${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 ?? []) { + 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`); } - const blob = await zip.generateAsync({ type: "blob" }); - saveAs(blob, `${eTrail.name}.zip`); } } catch (e) { console.error(e); @@ -235,23 +261,18 @@ } async function deleteTrails() { - if (trails !== undefined && trails.size > 0) { - for (const dTrail of trails) { + if (hasTrail()) { + for (const dTrail of trails!) { await doDeleteTrail(dTrail); } onconfirm?.(); } - else if (trail !== undefined) { - await doDeleteTrail(trail); - - setTimeout(() => { - goto("/trails"); - }, 500); - } } async function doDeleteTrail(dTrail: Trail) { + if (dTrail === undefined) return; + if (!allowDeleteTrail(dTrail)) return; await trails_delete(dTrail); @@ -262,18 +283,14 @@ let deleted = false; let multiple = false; - if (trails !== undefined && trails.size > 0) { + if (hasTrail()) { multiple = true; - for (const lTrail of trails) { + for (const lTrail of trails!) { if (await doHandleListSelection(list, lTrail)) { deleted = true; } } } - else if (trail !== undefined) - { - deleted = await doHandleListSelection(list, trail); - } if (deleted) { show_toast({ @@ -312,7 +329,7 @@ } - handleDropdownClick(item)} + handleDropdownClick(item)} >{#snippet children({ toggleMenu: openDropdown })}
{#if ($currentUser && $currentUser.id == trail.author) || trail.expand?.trail_share_via_trail?.length || trail.public} - + ([trail])} {mode}> {/if}
diff --git a/web/src/lib/components/trail/trail_list.svelte b/web/src/lib/components/trail/trail_list.svelte index e9a6aa9b..dcbad985 100644 --- a/web/src/lib/components/trail/trail_list.svelte +++ b/web/src/lib/components/trail/trail_list.svelte @@ -148,28 +148,36 @@ return false; } - function handleSelectionUpdate(sTrail: Trail) { - if (sTrail === undefined) { - let addTrails = false; - if (selection === undefined) { - selection = new Set(); - addTrails = true; - } - else { - addTrails = selection.size === 0 || selection.size !== trails.length; - selection.clear(); - } +function handleSelectionUpdate(trail: Trail) { + + let newSelection = new Set(); + + 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; + } - if (addTrails) { - trails.forEach((value: Trail) => selectTrail(value)); + newSelection.add(sTrail); } + } + + if (!isSelected) { + newSelection.add(trail); } - else { - selectTrail(sTrail); + } else if (selection === undefined || selection.size === 0 || (trails !== undefined && selection.size !== trails.length)) { + for (const eTrail of trails){ + newSelection.add(eTrail); } - - //onupdate?.(filter, selection); } + + selection = newSelection; +} function handleHoverUpdate(hTrail: Trail) { if (hTrail === undefined) { @@ -180,35 +188,6 @@ else hoveredTrail = undefined; } - function selectTrail(trail: Trail) { - if (trail !== undefined) { - if (selection === undefined) selection = new Set(); - - let exists = false; - for (const sTrail of selection) { - if (sTrail !== undefined && sTrail.id === trail.id) { - exists = true; - break; - } - } - - if (exists) { - let newSelection = new Set(); - for (const sTrail of selection) { - if (sTrail !== undefined && sTrail.id === trail.id) - continue; - - newSelection.add(sTrail); - } - - selection = newSelection; - } - else { - selection.add(trail); - } - } - } - function handleTrailsEditDone() { setTimeout(() => { onupdate?.(filter, selection); @@ -221,10 +200,6 @@ function handleMouseLeave(trail: Trail) { handleHoverUpdate(trail); } - function handeTrailSelect(trail: Trail) { - handleSelectionUpdate(trail); - //hoveredTrail = undefined; - }
@@ -236,12 +211,16 @@ {onpagination} >
+ {#if selection !== undefined && selection.size > 0} +
+ +
+ {/if} {#if filter} - {#if selection !== undefined && selection.size > 0} - - {/if}
- {#if selectedDisplayOption !== "table"}

{$_("sort")}

@@ -304,7 +283,7 @@ )} {filter} onsort={handleSortUpdate} - onselect={handleSelectionUpdate} + onTrailSelect={(t) => handleSelectionUpdate(t)} > {:else} {#each trails as trail} @@ -312,15 +291,17 @@ class="max-w-full flex-1" class:basis-full={selectedDisplayOption === "list"} href="/trail/view/{trail.id}" + onmouseenter={(e) => handleMouseEnter(trail)} + onmouseleave={(e) => handleMouseLeave(trail)} > {#if selectedDisplayOption === "cards"} handleMouseEnter(trail)} onmouseleave={(e) => handleMouseLeave(trail)} - onTrailSelect={() => handeTrailSelect(trail)} + onTrailSelect={() => handleSelectionUpdate(trail)} > {:else} - + handleSelectionUpdate(trail)}> {/if} {/each} diff --git a/web/src/lib/components/trail/trail_list_item.svelte b/web/src/lib/components/trail/trail_list_item.svelte index 42902e4a..d6e075af 100644 --- a/web/src/lib/components/trail/trail_list_item.svelte +++ b/web/src/lib/components/trail/trail_list_item.svelte @@ -16,9 +16,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 @@ -27,104 +36,123 @@ ? emptyStateTrailLight : emptyStateTrailDark, ); + + function handleInputClick(e: Event) { + e.stopPropagation(); + onTrailSelect?.(); + hovered = true; + }
  • -
    - {#if isVideoURL(thumbnail)} - - - {:else} - - {/if} -
    -
    -
    -

    - {trail.name} -

    - {#if trail.public && $currentUser} - - - - {/if} - {#if trail.expand?.trail_share_via_trail?.length} - - {/if} -
    - {#if trail.date} -

    - {new Date(trail.date).toLocaleDateString(undefined, { - month: "long", - day: "2-digit", - year: "numeric", - timeZone: "UTC", - })} -

    - {/if} - {#if trail.expand?.author} -

    - {$_("by")} +

    + {#if isVideoURL(thumbnail)} + + + {:else} avatar - {trail.expand.author.username} -

    - {/if} -
    - {#if trail.location} -
    {trail.location}
    {/if} -
    - {$_(trail.difficulty ?? "?")} -
    -
    +
    +
    + {#if hovered || selected} +
    + handleInputClick(e)} + /> +
    + {/if} +
    +
    +

    + {trail.name} +

    + {#if trail.public && $currentUser} + + + + {/if} + {#if trail.expand?.trail_share_via_trail?.length} + + {/if} +
    + {#if trail.date} +

    + {new Date(trail.date).toLocaleDateString(undefined, { + month: "long", + day: "2-digit", + year: "numeric", + timeZone: "UTC", + })} +

    + {/if} + {#if trail.expand?.author} +

    + {$_("by")} + avatar + {trail.expand.author.username} +

    + {/if} +
    + {#if trail.location} +
    {trail.location}
    + {/if} +
    + {$_(trail.difficulty ?? "?")} +
    +
    -
    - {formatDistance( - trail.distance, - )} - {formatTimeHHMM( - trail.duration, - )} - {formatElevation( - trail.elevation_gain, - )} - {formatElevation( - trail.elevation_loss, - )} -
    - {#if showDescription} -

    - {trail.description} -

    - {/if} +
    + {formatDistance( + trail.distance, + )} + {formatTimeHHMM( + trail.duration, + )} + {formatElevation( + trail.elevation_gain, + )} + {formatElevation( + trail.elevation_loss, + )} +
    + {#if showDescription} +

    + {trail.description} +

    + {/if} +
  • diff --git a/web/src/lib/components/trail/trail_share_modal.svelte b/web/src/lib/components/trail/trail_share_modal.svelte index 082ecc25..6f5a5d3f 100644 --- a/web/src/lib/components/trail/trail_share_modal.svelte +++ b/web/src/lib/components/trail/trail_share_modal.svelte @@ -58,7 +58,7 @@ if (trail === undefined) return; const share = new TrailShare(item.value.id, trail.id!, "view"); - await trail_share_create(share); + await trail_share_create(share); fetchShares(); } diff --git a/web/src/lib/components/trail/trail_table.svelte b/web/src/lib/components/trail/trail_table.svelte index d2fa6b6d..eeac020c 100644 --- a/web/src/lib/components/trail/trail_table.svelte +++ b/web/src/lib/components/trail/trail_table.svelte @@ -17,10 +17,10 @@ selection: Set | undefined; filter?: TrailFilter | null; onsort?: (value: any) => void - onselect?: (value: any) => void + onTrailSelect?: (value: any) => void } - let { tableHeader, trails = null, selection, filter = null, onsort, onselect }: Props = $props(); + let { tableHeader, trails = null, selection, filter = null, onsort, onTrailSelect: onselect }: Props = $props(); function getColumnWidth(columnValue: string): string { switch (columnValue) { @@ -138,7 +138,6 @@
    Date: Wed, 7 May 2025 19:00:00 +0200 Subject: [PATCH 04/10] remove unnecessary imports, move code to simplify diff of trail_dropdown --- .../lib/components/trail/trail_card.svelte | 3 +- .../components/trail/trail_dropdown.svelte | 41 +++++++++---------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/web/src/lib/components/trail/trail_card.svelte b/web/src/lib/components/trail/trail_card.svelte index fa80a20e..524ffa63 100644 --- a/web/src/lib/components/trail/trail_card.svelte +++ b/web/src/lib/components/trail/trail_card.svelte @@ -2,8 +2,8 @@ import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg"; import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg"; import type { Trail } from "$lib/models/trail"; - import { currentUser } from "$lib/stores/user_store"; import { theme } from "$lib/stores/theme_store"; + import { currentUser } from "$lib/stores/user_store"; import { getFileURL, isVideoURL } from "$lib/util/file_util"; import { formatDistance, @@ -11,7 +11,6 @@ formatTimeHHMM, } from "$lib/util/format_util"; import { _ } from "svelte-i18n"; - import ShareInfo from "../share_info.svelte"; import type { MouseEventHandler } from "svelte/elements"; import Chip from "../base/chip.svelte"; diff --git a/web/src/lib/components/trail/trail_dropdown.svelte b/web/src/lib/components/trail/trail_dropdown.svelte index 240dbd6e..78e75a46 100644 --- a/web/src/lib/components/trail/trail_dropdown.svelte +++ b/web/src/lib/components/trail/trail_dropdown.svelte @@ -20,7 +20,6 @@ import ListSelectModal from "../list/list_select_modal.svelte"; import TrailExportModal from "./trail_export_modal.svelte"; import TrailShareModal from "./trail_share_modal.svelte"; - import { any } from "zod"; interface Props { trails?: Set | undefined; @@ -47,26 +46,6 @@ )!; } - function isMultiselectMode() : boolean { - return trails !== undefined && trails.size > 1; - } - - function hasTrail() : boolean { - return trails !== undefined && trails.size > 0 && [...trails][0] !== undefined; - } - - function canExport() : boolean { - return !isMultiselectMode() && hasTrail() && trail()!.expand?.gpx_data != undefined && trail()!.expand!.gpx_data!.length > 0; - } - - function trailId() : string | undefined { - return trail()?.id; - } - - function trail() : Trail | undefined { - return hasTrail() ? [...trails!][0] : undefined; - } - function dropdownItems(): DropdownItem[] { return [ ...(!isMultiselectMode() @@ -115,6 +94,26 @@ ]; } + function isMultiselectMode() : boolean { + return trails !== undefined && trails.size > 1; + } + + function hasTrail() : boolean { + return trails !== undefined && trails.size > 0 && [...trails][0] !== undefined; + } + + function canExport() : boolean { + return !isMultiselectMode() && hasTrail() && trail()!.expand?.gpx_data != undefined && trail()!.expand!.gpx_data!.length > 0; + } + + function trailId() : string | undefined { + return trail()?.id; + } + + function trail() : Trail | undefined { + return hasTrail() ? [...trails!][0] : undefined; + } + function isFromCurrentUser(uTrail?: Trail) : boolean { if (uTrail !== undefined) { return uTrail.author === $currentUser?.id; From 67c61f3b38f09398273f93cb06a826072bb45551 Mon Sep 17 00:00:00 2001 From: brotkrume Date: Wed, 7 May 2025 20:37:51 +0200 Subject: [PATCH 05/10] translations --- web/src/lib/i18n/locales/de.json | 2 ++ web/src/lib/i18n/locales/en.json | 2 ++ web/src/lib/i18n/locales/es.json | 2 ++ web/src/lib/i18n/locales/fr.json | 2 ++ web/src/lib/i18n/locales/hu.json | 2 ++ web/src/lib/i18n/locales/it.json | 2 ++ web/src/lib/i18n/locales/nl.json | 2 ++ web/src/lib/i18n/locales/pl.json | 2 ++ web/src/lib/i18n/locales/pt.json | 2 ++ web/src/lib/i18n/locales/zh.json | 2 ++ 10 files changed, 20 insertions(+) diff --git a/web/src/lib/i18n/locales/de.json b/web/src/lib/i18n/locales/de.json index 1e824a5a..2de917af 100644 --- a/web/src/lib/i18n/locales/de.json +++ b/web/src/lib/i18n/locales/de.json @@ -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", @@ -257,6 +258,7 @@ "read-more": "Mehr", "register": "Registrieren", "removed-trail-from": "Route entfernt aus", + "removed-trails-from": "Routen entfernt aus", "required": "Pflichtfeld", "reset-password": "Passwort zurücksetzen", "road": "Straße", diff --git a/web/src/lib/i18n/locales/en.json b/web/src/lib/i18n/locales/en.json index b3c5daf6..1a4efc41 100644 --- a/web/src/lib/i18n/locales/en.json +++ b/web/src/lib/i18n/locales/en.json @@ -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", @@ -257,6 +258,7 @@ "read-more": "Read more", "register": "Register", "removed-trail-from": "Removed trail from", + "removed-trails-from": "Removed trails from", "required": "Required", "reset-password": "Reset Password", "road": "Road", diff --git a/web/src/lib/i18n/locales/es.json b/web/src/lib/i18n/locales/es.json index 2b02409f..5f5cef0b 100644 --- a/web/src/lib/i18n/locales/es.json +++ b/web/src/lib/i18n/locales/es.json @@ -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", @@ -257,6 +258,7 @@ "read-more": "Leer más", "register": "Registrar", "removed-trail-from": "Ruta borrada de", + "removed-trails-from": "Rutas borrada de", "required": "Obligatorio", "reset-password": "Restablecer Contraseña", "road": "Road", diff --git a/web/src/lib/i18n/locales/fr.json b/web/src/lib/i18n/locales/fr.json index 67e618bd..192bea9c 100644 --- a/web/src/lib/i18n/locales/fr.json +++ b/web/src/lib/i18n/locales/fr.json @@ -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", @@ -257,6 +258,7 @@ "read-more": "Voir plus", "register": "Créer un compte", "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", diff --git a/web/src/lib/i18n/locales/hu.json b/web/src/lib/i18n/locales/hu.json index ab8b189e..238cf429 100644 --- a/web/src/lib/i18n/locales/hu.json +++ b/web/src/lib/i18n/locales/hu.json @@ -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", @@ -257,6 +258,7 @@ "read-more": "Read more", "register": "Regisztráció", "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", diff --git a/web/src/lib/i18n/locales/it.json b/web/src/lib/i18n/locales/it.json index c8738dde..ca419a9d 100644 --- a/web/src/lib/i18n/locales/it.json +++ b/web/src/lib/i18n/locales/it.json @@ -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", @@ -257,6 +258,7 @@ "read-more": "Per saperne di più", "register": "Registrati", "removed-trail-from": "Percorso rimosso da", + "removed-trails-from": "Percorsi rimosso da", "required": "Obbligatorio", "reset-password": "Ripristinare Password", "road": "Road", diff --git a/web/src/lib/i18n/locales/nl.json b/web/src/lib/i18n/locales/nl.json index 70ebdaf6..1ff0e92e 100644 --- a/web/src/lib/i18n/locales/nl.json +++ b/web/src/lib/i18n/locales/nl.json @@ -14,6 +14,7 @@ "add-to-list": "Toevoegen aan lijst", "add-waypoint": "Routepunt toevoegen", "added-trail-to": "Wandelroute toegevoegd aan", + "added-trails-to": "Wandelroutes toegevoegd aan", "after": "After", "all-activities": "All activities", "alphabetical": "Alfabetisch", @@ -257,6 +258,7 @@ "read-more": "Read more", "register": "Registreren", "removed-trail-from": "De wandelroute is verwijderd van", + "removed-trails-from": "De wandelroutes zijn verwijderd van", "required": "Verplicht", "reset-password": "Reset Password", "road": "Road", diff --git a/web/src/lib/i18n/locales/pl.json b/web/src/lib/i18n/locales/pl.json index df141134..da95dafa 100644 --- a/web/src/lib/i18n/locales/pl.json +++ b/web/src/lib/i18n/locales/pl.json @@ -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", @@ -257,6 +258,7 @@ "read-more": "Czytaj dalej", "register": "Zarejestruj", "removed-trail-from": "Usunięto szlak z", + "removed-trails-from": "Usunięto szlaki z", "required": "Wymagane", "reset-password": "Resetuj hasło", "road": "Road", diff --git a/web/src/lib/i18n/locales/pt.json b/web/src/lib/i18n/locales/pt.json index 1c7496e4..2029240d 100644 --- a/web/src/lib/i18n/locales/pt.json +++ b/web/src/lib/i18n/locales/pt.json @@ -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", @@ -257,6 +258,7 @@ "read-more": "Ler mais", "register": "Registo", "removed-trail-from": "Trilha removida de", + "removed-trails-from": "Trilhos removidos de", "required": "Obrigatório", "reset-password": "Reset Password", "road": "Road", diff --git a/web/src/lib/i18n/locales/zh.json b/web/src/lib/i18n/locales/zh.json index b512759d..8433ee49 100644 --- a/web/src/lib/i18n/locales/zh.json +++ b/web/src/lib/i18n/locales/zh.json @@ -14,6 +14,7 @@ "add-to-list": "添加到列表", "add-waypoint": "添加坐标", "added-trail-to": "添加路线到", + "added-trails-to": "添加路线到", "after": "之后", "all-activities": "所有活动", "alphabetical": "字母", @@ -257,6 +258,7 @@ "read-more": "阅读更多", "register": "注册", "removed-trail-from": "路线已删除自", + "removed-trails-from": "路线已删除自", "required": "必填", "reset-password": "重置密码", "road": "Road", From f560511f7ed27fee54aefbbfd9c1a4b3d216ee8e Mon Sep 17 00:00:00 2001 From: brotkrume Date: Thu, 8 May 2025 10:34:07 +0200 Subject: [PATCH 06/10] fix horizontal scrollbar in table view --- web/src/lib/components/trail/trail_table.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/trail/trail_table.svelte b/web/src/lib/components/trail/trail_table.svelte index eeac020c..b537aa36 100644 --- a/web/src/lib/components/trail/trail_table.svelte +++ b/web/src/lib/components/trail/trail_table.svelte @@ -89,7 +89,7 @@
    From 01d12a3a2bcb1a1a0a62a2e85e117d3628711680 Mon Sep 17 00:00:00 2001 From: brotkrume Date: Thu, 8 May 2025 11:02:02 +0200 Subject: [PATCH 07/10] direct export from list, multiselect export --- .../lib/components/trail/trail_dropdown.svelte | 12 +++++++++++- web/src/lib/util/gpx_util.ts | 18 +++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/web/src/lib/components/trail/trail_dropdown.svelte b/web/src/lib/components/trail/trail_dropdown.svelte index 78e75a46..18395487 100644 --- a/web/src/lib/components/trail/trail_dropdown.svelte +++ b/web/src/lib/components/trail/trail_dropdown.svelte @@ -101,9 +101,19 @@ 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 !isMultiselectMode() && hasTrail() && trail()!.expand?.gpx_data != undefined && trail()!.expand!.gpx_data!.length > 0; + return hasGpx(); } function trailId() : string | undefined { diff --git a/web/src/lib/util/gpx_util.ts b/web/src/lib/util/gpx_util.ts index 0f9052c1..dbe31a00 100644 --- a/web/src/lib/util/gpx_util.ts +++ b/web/src/lib/util/gpx_util.ts @@ -14,6 +14,7 @@ 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"; export async function gpx2trail(gpxString: string, fallbackName?: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise = fetch) { @@ -71,10 +72,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!, 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 +104,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({ From 71e41a95ef862821f35ed0136cc7027d9218036a Mon Sep 17 00:00:00 2001 From: brotkrume Date: Thu, 8 May 2025 11:30:11 +0200 Subject: [PATCH 08/10] fix refreshing share icon after sharing from trail list --- web/src/lib/components/trail/trail_dropdown.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/lib/components/trail/trail_dropdown.svelte b/web/src/lib/components/trail/trail_dropdown.svelte index 18395487..7b10c546 100644 --- a/web/src/lib/components/trail/trail_dropdown.svelte +++ b/web/src/lib/components/trail/trail_dropdown.svelte @@ -287,6 +287,10 @@ await trails_delete(dTrail); } + async function handleShareUpdate() { + onconfirm?.(); + } + async function handleListSelection(list: List) { try { let deleted = false; @@ -364,4 +368,4 @@ bind:this={trailExportModal} onexport={(settings) => exportTrails(settings)} > - + From b5f8a6ee33aec5508df88f0e50190d12fb94da63 Mon Sep 17 00:00:00 2001 From: brotkrume Date: Thu, 8 May 2025 11:52:13 +0200 Subject: [PATCH 09/10] fix adding multiple trails to a trail-list --- .../components/list/list_select_modal.svelte | 20 +++++++++++-- .../components/trail/trail_dropdown.svelte | 28 ++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/web/src/lib/components/list/list_select_modal.svelte b/web/src/lib/components/list/list_select_modal.svelte index 003ec4a4..60116569 100644 --- a/web/src/lib/components/list/list_select_modal.svelte +++ b/web/src/lib/components/list/list_select_modal.svelte @@ -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 | 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 @@
    {list.name}
    diff --git a/web/src/lib/components/trail/trail_dropdown.svelte b/web/src/lib/components/trail/trail_dropdown.svelte index 7b10c546..489bf3ae 100644 --- a/web/src/lib/components/trail/trail_dropdown.svelte +++ b/web/src/lib/components/trail/trail_dropdown.svelte @@ -120,6 +120,10 @@ return trail()?.id; } + function getTrails() : Set | undefined { + return trails; + } + function trail() : Trail | undefined { return hasTrail() ? [...trails!][0] : undefined; } @@ -330,14 +334,29 @@ } } - async function doHandleListSelection(list: List, lTrail: Trail): Promise { + async function doHandleListSelection(list: List, lTrail: Trail): Promise { if (list.trails?.includes(lTrail.id!)) { - await lists_remove_trail(list, lTrail); - return true; + 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; } @@ -360,7 +379,8 @@ onconfirm={deleteTrails} > handleListSelection(list)} > From 816f985ca13798762ed5b9eaaad721e318c073d9 Mon Sep 17 00:00:00 2001 From: brotkrume Date: Thu, 8 May 2025 12:46:25 +0200 Subject: [PATCH 10/10] fix retrieving mail notification template --- db/util/email_templates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/util/email_templates.go b/db/util/email_templates.go index 817340cd..e52abc60 100644 --- a/db/util/email_templates.go +++ b/db/util/email_templates.go @@ -56,7 +56,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 {