new trail view options: create copy & change visibility (#571)
* change visibility of multiple trails in trails view (multi-select) * error handling * create copy option added * wait for meilisearch when updating trail * remove toggles from dropdown * change public/private logic * adds loading spinner for bulk operations * fixes trail duplicate * fixes tags delete on visiblility change * fix conflict resolving issue * edit labels & german translation --------- Co-authored-by: Christian Beutel <> Co-authored-by: Flomp <Flomp@users.noreply.github.com>
This commit is contained in:
@@ -346,11 +346,11 @@ func IndexTrails(app core.App, trails []*core.Record, client meilisearch.Service
|
||||
func UpdateTrail(app core.App, r *core.Record, author *core.Record, client meilisearch.ServiceManager) error {
|
||||
errs := app.ExpandRecord(r, []string{"tags"}, nil)
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("failed to expand tags: %v", errs)
|
||||
return fmt.Errorf("meilisearch update trail: failed to expand tags: %v", errs)
|
||||
}
|
||||
errs = app.ExpandRecord(r, []string{"category"}, nil)
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("failed to expand category: %v", errs)
|
||||
return fmt.Errorf("meilisearch update trail: failed to expand category: %v", errs)
|
||||
}
|
||||
|
||||
doc, err := documentFromTrailRecord(app, r, author, false)
|
||||
@@ -359,10 +359,18 @@ func UpdateTrail(app core.App, r *core.Record, author *core.Record, client meili
|
||||
}
|
||||
documents := []map[string]interface{}{doc}
|
||||
|
||||
if _, err := client.Index("trails").UpdateDocuments(documents); err != nil {
|
||||
task, err := client.Index("trails").UpdateDocuments(documents)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interval := 500 * time.Millisecond
|
||||
_, err = client.WaitForTask(task.TaskUID, interval)
|
||||
if err != nil {
|
||||
return fmt.Errorf("meilisearch update trail: error waiting for task completion: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
let dropdownElement: HTMLUListElement | undefined = $state();
|
||||
let dropdownToggleElement: HTMLDivElement;
|
||||
|
||||
export async function toggleMenu(e: MouseEvent) {
|
||||
export async function toggleMenu(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
@@ -81,7 +81,10 @@
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function handleItemClick(e: MouseEvent, item: { text: string; value: any }) {
|
||||
function handleItemClick(
|
||||
e: MouseEvent,
|
||||
item: { text: string; value: any },
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onchange?.(item);
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
import type { List } from "$lib/models/list";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { categories } from "$lib/stores/category_store.js";
|
||||
import {
|
||||
lists_add_trail,
|
||||
lists_index,
|
||||
lists_remove_trail,
|
||||
} from "$lib/stores/list_store";
|
||||
import { show_toast } from "$lib/stores/toast_store.svelte";
|
||||
import { trails_delete } from "$lib/stores/trail_store";
|
||||
import { trails_delete, trails_update } from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
import { getFileURL, saveAs } from "$lib/util/file_util";
|
||||
import { trail2gpx } from "$lib/util/gpx_util";
|
||||
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
||||
import JSZip from "jszip";
|
||||
import type { Snippet } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
|
||||
import ConfirmModal from "../confirm_modal.svelte";
|
||||
import ListSearchModal from "../list/list_search_modal.svelte";
|
||||
import TrailExportModal from "./trail_export_modal.svelte";
|
||||
import TrailShareModal from "./trail_share_modal.svelte";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
import type { Snippet } from "svelte";
|
||||
import { page } from "$app/state";
|
||||
|
||||
interface Props {
|
||||
trails?: Set<Trail> | undefined;
|
||||
@@ -30,9 +31,10 @@
|
||||
toggle?: Snippet<[any]>;
|
||||
onDelete?: () => void;
|
||||
onShare?: () => void;
|
||||
onUpdate?: () => void;
|
||||
}
|
||||
|
||||
let { trails, mode, toggle, onDelete, onShare }: Props = $props();
|
||||
let { trails, mode, toggle, onDelete, onShare, onUpdate }: Props = $props();
|
||||
|
||||
let confirmModal: ConfirmModal;
|
||||
let listSelectModal: ListSearchModal;
|
||||
@@ -41,6 +43,8 @@
|
||||
|
||||
let lists: List[] = $state([]);
|
||||
|
||||
let loading: boolean = $state(false);
|
||||
|
||||
function allowEdit(): boolean {
|
||||
return (
|
||||
hasTrail() &&
|
||||
@@ -53,6 +57,61 @@
|
||||
);
|
||||
}
|
||||
|
||||
function majorityOfSelectedTrailsArePublic(): boolean {
|
||||
if (trails === undefined || trails.size === 0) return false;
|
||||
|
||||
if (!Boolean($currentUser)) return false;
|
||||
|
||||
let publicCount = 0;
|
||||
|
||||
for (const cTrail of trails) {
|
||||
if (cTrail.expand?.author === undefined) return false;
|
||||
if (
|
||||
cTrail.expand!.author!.id !== $currentUser?.actor &&
|
||||
!cTrail.expand?.trail_share_via_trail?.some(
|
||||
(s) => s.permission == "edit",
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cTrail.public) {
|
||||
publicCount += 1;
|
||||
}
|
||||
}
|
||||
return publicCount >= trails.size / 2;
|
||||
}
|
||||
|
||||
function allowCopy(): boolean {
|
||||
if ((trails?.size ?? 0) > 1) return false;
|
||||
|
||||
return !isMultiselectMode();
|
||||
}
|
||||
|
||||
function allowPublish(): boolean {
|
||||
if (mode !== "multi-select") return false;
|
||||
|
||||
if (trails === undefined || trails.size === 0) return false;
|
||||
|
||||
if (!Boolean($currentUser)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const cTrail of trails) {
|
||||
if (cTrail.expand?.author === undefined) return false;
|
||||
if (
|
||||
cTrail.expand!.author!.id !== $currentUser?.actor &&
|
||||
!cTrail.expand?.trail_share_via_trail?.some(
|
||||
(s) => s.permission == "edit",
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function dropdownItems(): DropdownItem[] {
|
||||
return [
|
||||
...(!isMultiselectMode()
|
||||
@@ -71,7 +130,13 @@
|
||||
]
|
||||
: []),
|
||||
...(!isMultiselectMode()
|
||||
? [{ text: $_("directions"), value: "direction", icon: "car" }]
|
||||
? [
|
||||
{
|
||||
text: $_("directions"),
|
||||
value: "direction",
|
||||
icon: "car",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(canExport()
|
||||
? [
|
||||
@@ -83,25 +148,69 @@
|
||||
]
|
||||
: []),
|
||||
...(!isMultiselectMode()
|
||||
? [{ text: $_("print"), value: "print", icon: "print" }]
|
||||
? [
|
||||
{
|
||||
text: $_("print"),
|
||||
value: "print",
|
||||
icon: "print",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isFromCurrentUser()
|
||||
? []
|
||||
: [
|
||||
{
|
||||
text: $_("add-to-list"),
|
||||
value: "list",
|
||||
value: "print",
|
||||
icon: "bookmark",
|
||||
},
|
||||
]),
|
||||
...(isMultiselectMode() || !isFromCurrentUser()
|
||||
? []
|
||||
: [{ text: $_("share"), value: "share", icon: "share" }]),
|
||||
: [
|
||||
{
|
||||
text: $_("share"),
|
||||
value: "share",
|
||||
icon: "share",
|
||||
},
|
||||
]),
|
||||
...(allowCopy()
|
||||
? [
|
||||
{
|
||||
text: $_("duplicate"),
|
||||
value: "copy",
|
||||
icon: "copy",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(allowPublish()
|
||||
? [
|
||||
{
|
||||
text: `${majorityOfSelectedTrailsArePublic() ? $_("set-private") : $_("set-public")}`,
|
||||
value: "publish",
|
||||
icon: majorityOfSelectedTrailsArePublic()
|
||||
? "lock"
|
||||
: "globe",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(allowEdit()
|
||||
? [{ text: $_("edit"), value: "edit", icon: "pen" }]
|
||||
? [
|
||||
{
|
||||
text: $_("edit"),
|
||||
value: "edit",
|
||||
icon: "pen",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(allowDelete()
|
||||
? [{ text: $_("delete"), value: "delete", icon: "trash" }]
|
||||
? [
|
||||
{
|
||||
text: $_("delete"),
|
||||
value: "delete",
|
||||
icon: "trash",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
@@ -174,19 +283,20 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const handle = page.params.handle ?? handleFromRecordWithIRI(trail())
|
||||
const handle = page.params.handle ?? handleFromRecordWithIRI(trail());
|
||||
|
||||
if (item.value == "show") {
|
||||
const ddVal = item.value as string;
|
||||
|
||||
if (ddVal == "show") {
|
||||
if (hasTrail()) {
|
||||
const url = mode == "overview" || mode == "multi-select"
|
||||
const url =
|
||||
mode == "overview" || mode == "multi-select"
|
||||
? `/map/trail/${handle}/${trailId()}`
|
||||
: `/trail/view/${handle}/${trailId()}`
|
||||
|
||||
goto(
|
||||
url + '?' + page.url.searchParams
|
||||
);
|
||||
: `/trail/view/${handle}/${trailId()}`;
|
||||
|
||||
goto(url + "?" + page.url.searchParams);
|
||||
}
|
||||
} else if (item.value == "list") {
|
||||
} else if (ddVal == "list") {
|
||||
lists = (
|
||||
await lists_index(
|
||||
{ q: "", author: $currentUser?.actor ?? "" },
|
||||
@@ -195,7 +305,7 @@
|
||||
)
|
||||
).items;
|
||||
listSelectModal.openModal();
|
||||
} else if (item.value == "direction") {
|
||||
} else if (ddVal == "direction") {
|
||||
if (hasTrail()) {
|
||||
window
|
||||
.open(
|
||||
@@ -204,23 +314,72 @@
|
||||
)
|
||||
?.focus();
|
||||
}
|
||||
} else if (item.value == "print") {
|
||||
} else if (ddVal == "print") {
|
||||
if (hasTrail()) {
|
||||
goto(`/map/trail/${handle}/${trailId()}/print?${page.url.searchParams}`);
|
||||
goto(
|
||||
`/map/trail/${handle}/${trailId()}/print?${page.url.searchParams}`,
|
||||
);
|
||||
}
|
||||
} else if (item.value == "share") {
|
||||
} else if (ddVal == "share") {
|
||||
trailShareModal.openModal();
|
||||
} else if (item.value == "download") {
|
||||
} else if (ddVal == "download") {
|
||||
trailExportModal.openModal();
|
||||
} else if (item.value == "edit") {
|
||||
} else if (ddVal == "edit") {
|
||||
if (hasTrail()) {
|
||||
goto(`/trail/edit/${trailId()}`);
|
||||
}
|
||||
} else if (item.value == "delete") {
|
||||
} else if (ddVal == "copy") {
|
||||
if (hasTrail()) {
|
||||
goto("/trail/edit/new?orig=" + trail()?.id);
|
||||
}
|
||||
} else if (ddVal == "publish") {
|
||||
updateTrailsVisibility();
|
||||
} else if (ddVal == "delete") {
|
||||
confirmModal.openModal();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTrailsVisibility() {
|
||||
const newVisibility = !majorityOfSelectedTrailsArePublic();
|
||||
|
||||
loading = true;
|
||||
for (const cTrail of trails ?? []) {
|
||||
if (!cTrail) continue;
|
||||
|
||||
if (!cTrail.expand?.author?.id) continue;
|
||||
|
||||
const origTrail: Trail = {
|
||||
...cTrail,
|
||||
author: cTrail.expand!.author!.id,
|
||||
};
|
||||
const updatedTrail: Trail = {
|
||||
...origTrail,
|
||||
public: newVisibility,
|
||||
};
|
||||
|
||||
try {
|
||||
await trails_update(
|
||||
origTrail,
|
||||
updatedTrail,
|
||||
undefined,
|
||||
undefined,
|
||||
["tags", "category"],
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: `${$_("error-saving-trail")}: ${cTrail.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
onUpdate?.();
|
||||
}
|
||||
|
||||
async function exportTrails(exportSettings: {
|
||||
fileFormat: "gpx" | "json";
|
||||
photos: boolean;
|
||||
@@ -308,9 +467,12 @@
|
||||
|
||||
async function deleteTrails() {
|
||||
if (hasTrail()) {
|
||||
loading = true;
|
||||
|
||||
for (const dTrail of trails!) {
|
||||
await doDeleteTrail(dTrail);
|
||||
}
|
||||
loading = false;
|
||||
|
||||
onDelete?.();
|
||||
}
|
||||
@@ -407,7 +569,11 @@
|
||||
{#snippet children({ toggleMenu: openDropdown })}
|
||||
{#if toggle}{@render toggle({
|
||||
toggleMenu: openDropdown,
|
||||
})}{:else if mode == "multi-select"}
|
||||
})}
|
||||
{:else if loading}
|
||||
<div class:w-16={isMultiselectMode()}></div>
|
||||
<div class="spinner light:spinner-dark"></div>
|
||||
{:else if mode == "multi-select"}
|
||||
<button
|
||||
aria-label="Open dropdown"
|
||||
class="btn-primary shrink-0 font-medium!"
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
|
||||
async function handleTrailsEditDone(resetSelection: boolean = false) {
|
||||
if (resetSelection) {
|
||||
selection?.clear();
|
||||
selection = new Set<Trail>();
|
||||
hoveredTrail = undefined;
|
||||
}
|
||||
await tick();
|
||||
@@ -319,6 +319,7 @@
|
||||
mode={"multi-select"}
|
||||
onDelete={() => handleTrailsEditDone(true)}
|
||||
onShare={() => handleTrailsEditDone(false)}
|
||||
onUpdate={() => handleTrailsEditDone(true)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"alphabetical": "Alphabetisch",
|
||||
"already-account": "Du hast bereits ein Konto?",
|
||||
"altitude": "Höhe",
|
||||
"amenity": "",
|
||||
"ammenity": "Einrichtung",
|
||||
"api-documentation": "API Dokumentation",
|
||||
"apply-user-settings": "",
|
||||
@@ -113,7 +114,7 @@
|
||||
"download": "Herunterladen",
|
||||
"draw-a-route": "Route zeichnen",
|
||||
"driving": "Auto",
|
||||
"duplicate": "Duplikat",
|
||||
"duplicate": "Duplizieren",
|
||||
"duration": "Dauer",
|
||||
"dutch": "Niederländisch",
|
||||
"easy": "Einfach",
|
||||
@@ -136,6 +137,7 @@
|
||||
"enable-auto-routing": "Auto-Routing aktivieren",
|
||||
"english": "Englisch",
|
||||
"entry": "Eintrag",
|
||||
"error-copying-trail": "Fehler beim Kopieren der Route",
|
||||
"error-creating-user": "Fehler beim Erstellen des Nutzers",
|
||||
"error-disabling-strava-integration": "Fehler beim Deaktivieren der Stravaintegration",
|
||||
"error-during-login": "Fehler beim Login",
|
||||
@@ -358,6 +360,8 @@
|
||||
"search-trails": "Route suchen",
|
||||
"select-list": "Liste auswählen",
|
||||
"selected": "ausgewählt",
|
||||
"set-private": "Verbergen",
|
||||
"set-public": "Veröffentlichen",
|
||||
"settings": "Einstellungen",
|
||||
"settings-notification-comment-mention": "Jemand hat dich in einem Kommentar erwähnt",
|
||||
"settings-notification-list-create": "Ein Benutzer, dem Du folgst, hat eine Liste erstellt",
|
||||
@@ -413,6 +417,7 @@
|
||||
"top-speed": "Höchstgeschwindigkeit",
|
||||
"tourism": "Tourismus",
|
||||
"trail": "{n, plural, =1 {Route} other {Routen}}",
|
||||
"trail-copied-successfully": "Route erfolgreich kopiert",
|
||||
"trail-not-in-list": "Trail gehört zu keiner Liste.",
|
||||
"trail-not-shared": "Mit niemandem geteilt",
|
||||
"trail-saved-successfully": "Route gespeichert",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"already-account": "Already have an account?",
|
||||
"altitude": "Altitude",
|
||||
"amenity": "Amenity",
|
||||
"ammenity": "",
|
||||
"api-documentation": "API Documentation",
|
||||
"apply-user-settings": "Apply user settings",
|
||||
"attraction": "Attraction",
|
||||
@@ -136,6 +137,7 @@
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "English",
|
||||
"entry": "Entry",
|
||||
"error-copying-trail": "Error copying trail",
|
||||
"error-creating-user": "Error creating user",
|
||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||
"error-during-login": "Error during login",
|
||||
@@ -358,6 +360,8 @@
|
||||
"search-trails": "Search trails",
|
||||
"select-list": "Select List",
|
||||
"selected": "selected",
|
||||
"set-private": "Set private",
|
||||
"set-public": "Set public",
|
||||
"settings": "Settings",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
@@ -413,6 +417,7 @@
|
||||
"top-speed": "Top Speed",
|
||||
"tourism": "Tourism",
|
||||
"trail": "{n, plural, =1 {Trail} other {Trails}}",
|
||||
"trail-copied-successfully": "trail copied successfully",
|
||||
"trail-not-in-list": "Trail is not in any list",
|
||||
"trail-not-shared": "Not shared with anyone",
|
||||
"trail-saved-successfully": "Trail saved successfully",
|
||||
@@ -430,6 +435,7 @@
|
||||
"username": "Username",
|
||||
"view": "View",
|
||||
"viewpoint": "Viewpoint",
|
||||
"visibilty": "Visibility",
|
||||
"visibilty-status": "Visibility status",
|
||||
"walking-speed": "Walking speed",
|
||||
"water": "Water",
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
"alphabetical": "Alfabético",
|
||||
"already-account": "¿Ya tienes una cuenta?",
|
||||
"altitude": "Altitud",
|
||||
"amenity": "",
|
||||
"ammenity": "Servicios",
|
||||
"api-documentation": "Documentación API",
|
||||
"apply-user-settings": "",
|
||||
"attraction": "Atracción",
|
||||
"author": "Autor",
|
||||
"avatar": "Avatar",
|
||||
@@ -134,6 +136,7 @@
|
||||
"enable-auto-routing": "Habilitar auto-ruta",
|
||||
"english": "Inglés",
|
||||
"entry": "Entrada",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "Error creando el usuario",
|
||||
"error-disabling-strava-integration": "Error al desactivar la integración de strava",
|
||||
"error-during-login": "Error durante el acceso",
|
||||
@@ -356,6 +359,8 @@
|
||||
"search-trails": "Buscar ruta",
|
||||
"select-list": "Seleccionar Lista",
|
||||
"selected": "seleccionado",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "Configuración",
|
||||
"settings-notification-comment-mention": "Alguien te mencionó en un comentario",
|
||||
"settings-notification-list-create": "Un usuario al que sigues ha creado una nueva lista",
|
||||
@@ -411,6 +416,7 @@
|
||||
"top-speed": "Velocidad máxima",
|
||||
"tourism": "Turismo",
|
||||
"trail": "{n, plural, one {}=1 {Ruta} other {Rutas}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "No compartida con nadie",
|
||||
"trail-saved-successfully": "Ruta guardada con éxito",
|
||||
@@ -428,6 +434,7 @@
|
||||
"username": "Nombre de usuario",
|
||||
"view": "Ver",
|
||||
"viewpoint": "Mirador",
|
||||
"visibilty": "Visibilidad",
|
||||
"visibilty-status": "Estado de visibilidad",
|
||||
"walking-speed": "Velocidad al caminar",
|
||||
"water": "Agua",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"already-account": "Baduzu kontua lehendik?",
|
||||
"altitude": "Altuera",
|
||||
"amenity": "Altimetria",
|
||||
"ammenity": "",
|
||||
"api-documentation": "API dokumentazioa",
|
||||
"apply-user-settings": "",
|
||||
"attraction": "Erakarmena",
|
||||
@@ -135,6 +136,7 @@
|
||||
"enable-auto-routing": "Aktibatu bideratze automatikoa",
|
||||
"english": "Ingelesa",
|
||||
"entry": "Sarrera",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "Errorea erabiltzailea sortzen",
|
||||
"error-disabling-strava-integration": "Errorea stravarekin integrazioa desaktibatzean",
|
||||
"error-during-login": "Errorea sartzean",
|
||||
@@ -357,6 +359,8 @@
|
||||
"search-trails": "Bilatu ibilaldiak",
|
||||
"select-list": "Aukeratu zerrenda",
|
||||
"selected": "aukeratuta",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "Ezarpenak",
|
||||
"settings-notification-comment-mention": "Norbaitek iruzkin baten aipatu zaitu",
|
||||
"settings-notification-list-create": "Jarraitzen duzun erabiltzaile batek zerrenda bat sortu du",
|
||||
@@ -412,6 +416,7 @@
|
||||
"top-speed": "Gehienezko abiadura",
|
||||
"tourism": "Turismoa",
|
||||
"trail": "{n, plural, one {}=1 {ibilbide} other {ibilbide}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "Inorekin partekatu gabe",
|
||||
"trail-saved-successfully": "Ibilbidea ondo gorde da",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"alphabetical": "Alphabétique",
|
||||
"already-account": "Déjà un compte ?",
|
||||
"altitude": "Altitude",
|
||||
"amenity": "",
|
||||
"ammenity": "Aménagement",
|
||||
"api-documentation": "Documentation API",
|
||||
"apply-user-settings": "",
|
||||
@@ -135,6 +136,7 @@
|
||||
"enable-auto-routing": "Activer le routage automatique",
|
||||
"english": "Anglais",
|
||||
"entry": "Entrée",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "Erreur durant la création de l'utilisateur",
|
||||
"error-disabling-strava-integration": "Erreur lors de la désactivation de l'intégration Strava",
|
||||
"error-during-login": "Erreur durant la connexion",
|
||||
@@ -357,6 +359,8 @@
|
||||
"search-trails": "Chercher un itinéraire",
|
||||
"select-list": "Liste de choix",
|
||||
"selected": "sélectionné(s)",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "Paramètres",
|
||||
"settings-notification-comment-mention": "Quelqu’un vous a mentionné dans un commentaire",
|
||||
"settings-notification-list-create": "Un utilisateur que vous suivez à créé une nouvelle liste",
|
||||
@@ -412,6 +416,7 @@
|
||||
"top-speed": "Vitesse maximale",
|
||||
"tourism": "Tourisme",
|
||||
"trail": "{n, plural, =1 {Itinéraire} other {Itinéraires}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "L'itinéraire n'a pas été partagé",
|
||||
"trail-saved-successfully": "Itinéraire enregistrée",
|
||||
@@ -429,6 +434,7 @@
|
||||
"username": "Nom d'utilisateur",
|
||||
"view": "Afficher",
|
||||
"viewpoint": "Point de vue",
|
||||
"visibilty": "Visibilité",
|
||||
"visibilty-status": "État de visibilité",
|
||||
"walking-speed": "Vitesse de marche",
|
||||
"water": "Eau",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"already-account": "Már rendelkezik fiókkal?",
|
||||
"altitude": "Magasság",
|
||||
"amenity": "Amenity",
|
||||
"ammenity": "",
|
||||
"api-documentation": "API Dokumentáció",
|
||||
"apply-user-settings": "",
|
||||
"attraction": "Attraction",
|
||||
@@ -135,6 +136,7 @@
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "Angol",
|
||||
"entry": "Bejegyzés",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "Hiba felhasználó hozzáadása közben",
|
||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||
"error-during-login": "Hiba bejelentkezés közben",
|
||||
@@ -357,6 +359,8 @@
|
||||
"search-trails": "Nyomvonalak keresése",
|
||||
"select-list": "Lista kiválasztása",
|
||||
"selected": "selected",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "Beállítások",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
@@ -412,6 +416,7 @@
|
||||
"top-speed": "Top Speed",
|
||||
"tourism": "Tourism",
|
||||
"trail": "{n, plural, =1 {Útvonal} other {Útvonalak}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "Not shared with anyone",
|
||||
"trail-saved-successfully": "Trail saved successfully",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"already-account": "Hai già un account?",
|
||||
"altitude": "Altitudine",
|
||||
"amenity": "Amenity",
|
||||
"ammenity": "",
|
||||
"api-documentation": "Documentazione API",
|
||||
"apply-user-settings": "",
|
||||
"attraction": "Attraction",
|
||||
@@ -135,6 +136,7 @@
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "Inglese",
|
||||
"entry": "Voce",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "Errore nella creazione dell'utente",
|
||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||
"error-during-login": "Errore durante il login",
|
||||
@@ -357,6 +359,8 @@
|
||||
"search-trails": "Cerca percorsi",
|
||||
"select-list": "Seleziona lista",
|
||||
"selected": "selected",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "Impostazioni",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "Un utente che segui ha creato una lista",
|
||||
@@ -412,6 +416,7 @@
|
||||
"top-speed": "Top Speed",
|
||||
"tourism": "Tourism",
|
||||
"trail": "{n, plural, =1 {Percorso} other {Percorsi}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "Percorso non condiviso con nessuno",
|
||||
"trail-saved-successfully": "Percorso salvato con successo",
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
"enable-auto-routing": "Automatische routering inschakelen",
|
||||
"english": "Engels",
|
||||
"entry": "Item",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "Fout bij aanmaken gebruiker",
|
||||
"error-disabling-strava-integration": "Fout bij het uitschakelen van Strava-integratie",
|
||||
"error-during-login": "Het inloggen is mislukt",
|
||||
@@ -358,6 +359,8 @@
|
||||
"search-trails": "Zoek routes",
|
||||
"select-list": "Kies een lijst",
|
||||
"selected": "geselecteerd",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "Instellingen",
|
||||
"settings-notification-comment-mention": "Iemand vermeldt je in een reactie",
|
||||
"settings-notification-list-create": "Een gebruiker die je volgt, heeft een lijst gecreëerd",
|
||||
@@ -413,6 +416,7 @@
|
||||
"top-speed": "Topsnelheid",
|
||||
"tourism": "Toerisme",
|
||||
"trail": "{n, plural, =1 {Route} other {Routes}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "Niet gedeeld met iemand",
|
||||
"trail-saved-successfully": "Route succesvol bewaard",
|
||||
@@ -430,6 +434,7 @@
|
||||
"username": "Gebruikersnaam",
|
||||
"view": "Weergave",
|
||||
"viewpoint": "Uitzichtpunt",
|
||||
"visibilty": "Zichtbaarheid",
|
||||
"visibilty-status": "Zichtbaarheid status",
|
||||
"walking-speed": "Wandelsnelheid",
|
||||
"water": "Water",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"already-account": "Czy masz już konto?",
|
||||
"altitude": "Wysokość",
|
||||
"amenity": "Amenity",
|
||||
"ammenity": "",
|
||||
"api-documentation": "Dokumentacja API",
|
||||
"apply-user-settings": "",
|
||||
"attraction": "Attraction",
|
||||
@@ -135,6 +136,7 @@
|
||||
"enable-auto-routing": "Włącz auto-trasowanie",
|
||||
"english": "Angielski",
|
||||
"entry": "Pozycja",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "Błąd tworzenia użytkownika",
|
||||
"error-disabling-strava-integration": "Błąd przy wyłączaniu integracji strava",
|
||||
"error-during-login": "Błąd podczas logowania",
|
||||
@@ -357,6 +359,8 @@
|
||||
"search-trails": "Szukaj szlaków",
|
||||
"select-list": "Wybierz Listę",
|
||||
"selected": "selected",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "Ustawienia",
|
||||
"settings-notification-comment-mention": "Ktoś wspomniał o tobie w komentarzu",
|
||||
"settings-notification-list-create": "Użytkownik, którego obserwujesz, utworzył listę",
|
||||
@@ -412,6 +416,7 @@
|
||||
"top-speed": "Maksymalna prędkość",
|
||||
"tourism": "Tourism",
|
||||
"trail": "{n, plural, one {Szlak} few {Szlaki} many {Szlaków}=1 {Szlak} other {Szlaki}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "Szlak nie udostępniony",
|
||||
"trail-saved-successfully": "Szlak pomyślnie zapisany",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"already-account": "Já tem uma conta?",
|
||||
"altitude": "Altitude",
|
||||
"amenity": "Amenity",
|
||||
"ammenity": "",
|
||||
"api-documentation": "Documentação da API",
|
||||
"apply-user-settings": "",
|
||||
"attraction": "Attraction",
|
||||
@@ -135,6 +136,7 @@
|
||||
"enable-auto-routing": "Enable auto-routing",
|
||||
"english": "Inglês",
|
||||
"entry": "Entrada",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "Erro ao criar utilizador",
|
||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||
"error-during-login": "Erro durante o ‘login’",
|
||||
@@ -357,6 +359,8 @@
|
||||
"search-trails": "Procurar trilhos",
|
||||
"select-list": "Selecionar lista",
|
||||
"selected": "selected",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "Definições",
|
||||
"settings-notification-comment-mention": "Someone mentioned you in a comment",
|
||||
"settings-notification-list-create": "A user who you follow has created a list",
|
||||
@@ -412,6 +416,7 @@
|
||||
"top-speed": "Top Speed",
|
||||
"tourism": "Tourism",
|
||||
"trail": "{n, plural, =1 {Percurso} other {Percursos}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "Não partilhado com ninguém",
|
||||
"trail-saved-successfully": "Percurso gravado com sucesso",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"already-account": "Уже есть аккаунт?",
|
||||
"altitude": "Высота",
|
||||
"amenity": "Amenity",
|
||||
"ammenity": "",
|
||||
"api-documentation": "Документация API",
|
||||
"apply-user-settings": "",
|
||||
"attraction": "Attraction",
|
||||
@@ -135,6 +136,7 @@
|
||||
"enable-auto-routing": "Авто-маршрутизация",
|
||||
"english": "Английский",
|
||||
"entry": "Запись",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "Ошибка создания пользователя",
|
||||
"error-disabling-strava-integration": "Ошибка отключения Strava",
|
||||
"error-during-login": "Ошибка входа",
|
||||
@@ -357,6 +359,8 @@
|
||||
"search-trails": "Поиск треков",
|
||||
"select-list": "Выбрать список",
|
||||
"selected": "selected",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "Настройки",
|
||||
"settings-notification-comment-mention": "Кто-то упомянул вас в комментариях",
|
||||
"settings-notification-list-create": "Пользователь, на которого вы подписаны, создал список",
|
||||
@@ -412,6 +416,7 @@
|
||||
"top-speed": "Макс. скорость",
|
||||
"tourism": "Туризм",
|
||||
"trail": "{n, plural, =1 {Трек} other {Треки}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "Нет общего доступа",
|
||||
"trail-saved-successfully": "Трек сохранён",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"already-account": "已注册账户?",
|
||||
"altitude": "海拔",
|
||||
"amenity": "友好性",
|
||||
"ammenity": "",
|
||||
"api-documentation": "API 文档",
|
||||
"apply-user-settings": "",
|
||||
"attraction": "景点",
|
||||
@@ -135,6 +136,7 @@
|
||||
"enable-auto-routing": "启用自动路由",
|
||||
"english": "英语",
|
||||
"entry": "日程",
|
||||
"error-copying-trail": "",
|
||||
"error-creating-user": "创建用户错误",
|
||||
"error-disabling-strava-integration": "禁用strava集成时出错",
|
||||
"error-during-login": "登录错误",
|
||||
@@ -357,6 +359,8 @@
|
||||
"search-trails": "搜索路线",
|
||||
"select-list": "选择列表",
|
||||
"selected": "已选",
|
||||
"set-private": "",
|
||||
"set-public": "",
|
||||
"settings": "设置",
|
||||
"settings-notification-comment-mention": "有人在评论中提到您",
|
||||
"settings-notification-list-create": "您关注的用户创建了一个列表",
|
||||
@@ -412,6 +416,7 @@
|
||||
"top-speed": "最高速度",
|
||||
"tourism": "旅游",
|
||||
"trail": "{n, plural, =1 {路线} other {路线}}",
|
||||
"trail-copied-successfully": "",
|
||||
"trail-not-in-list": "",
|
||||
"trail-not-shared": "未与任何人分享",
|
||||
"trail-saved-successfully": "路线保存成功",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import cryptoRandomString from "crypto-random-string";
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
import type { Category } from "./category";
|
||||
import type { Comment } from "./comment";
|
||||
@@ -6,7 +7,7 @@ import type { SummitLog } from "./summit_log";
|
||||
import type { Tag } from "./tag";
|
||||
import type { TrailLike } from "./trail_like";
|
||||
import type { TrailShare } from "./trail_share";
|
||||
import type { Waypoint } from "./waypoint";
|
||||
import { Waypoint } from "./waypoint";
|
||||
|
||||
class Trail {
|
||||
id?: string;
|
||||
@@ -64,12 +65,13 @@ class Trail {
|
||||
thumbnail?: number,
|
||||
photos?: string[],
|
||||
gpx?: string,
|
||||
gpx_data?: string,
|
||||
category?: Category,
|
||||
waypoints?: Waypoint[],
|
||||
summit_logs?: SummitLog[],
|
||||
comments?: Comment[],
|
||||
shares?: TrailShare[],
|
||||
tags?: string[],
|
||||
tags?: Tag[],
|
||||
description?: string
|
||||
created?: string
|
||||
}
|
||||
@@ -89,7 +91,7 @@ class Trail {
|
||||
this.lon = params?.lon;
|
||||
this.thumbnail = params?.thumbnail ?? 0;
|
||||
this.photos = params?.photos ?? [];
|
||||
this.tags = []
|
||||
this.tags = [];
|
||||
this.gpx = params?.gpx;
|
||||
this.like_count = 0
|
||||
this.expand = {
|
||||
@@ -97,12 +99,39 @@ class Trail {
|
||||
waypoints_via_trail: params?.waypoints ?? [],
|
||||
summit_logs_via_trail: params?.summit_logs ?? [],
|
||||
comments_via_trail: params?.comments ?? [],
|
||||
trail_share_via_trail: params?.shares ?? []
|
||||
trail_share_via_trail: params?.shares ?? [],
|
||||
gpx_data: params?.gpx_data,
|
||||
tags: params?.tags
|
||||
}
|
||||
this.description = params?.description ?? "";
|
||||
this.created = params?.created;
|
||||
this.author = "000000000000000"
|
||||
}
|
||||
|
||||
static from(orig: Trail): Trail {
|
||||
return new Trail(orig.name, {
|
||||
date: orig.date,
|
||||
description: orig.description,
|
||||
difficulty: orig.difficulty,
|
||||
distance: orig.distance,
|
||||
duration: orig.duration,
|
||||
elevation_gain: orig.elevation_gain,
|
||||
elevation_loss: orig.elevation_loss,
|
||||
lat: orig.lat,
|
||||
lon: orig.lon,
|
||||
location: orig.location,
|
||||
public: orig.public,
|
||||
tags: orig.expand?.tags,
|
||||
category: orig.expand?.category,
|
||||
gpx_data: orig.expand?.gpx_data,
|
||||
waypoints: orig.expand?.waypoints_via_trail?.map(wp => new Waypoint(wp.lat, wp.lon, {
|
||||
id: cryptoRandomString({ length: 15 }),
|
||||
description: wp.description,
|
||||
icon: wp.icon,
|
||||
name: wp.name,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
interface TrailFilter {
|
||||
|
||||
@@ -238,7 +238,7 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
||||
|
||||
}
|
||||
|
||||
export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: File[], gpx?: File | Blob | null) {
|
||||
export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: File[], gpx?: File | Blob | null, exclude?: (keyof Trail)[]) {
|
||||
newTrail.author = oldTrail.author
|
||||
|
||||
const waypointUpdates = compareObjectArrays<Waypoint>(oldTrail.expand?.waypoints_via_trail ?? [], newTrail.expand?.waypoints_via_trail ?? []);
|
||||
@@ -295,7 +295,7 @@ export async function trails_update(oldTrail: Trail, newTrail: Trail, photos?: F
|
||||
newTrail.tags = newTrail.tags.filter(t => t != tag.id);
|
||||
}
|
||||
|
||||
const formData = objectToFormData(newTrail, ["expand"])
|
||||
const formData = objectToFormData(newTrail, ["expand", ...(exclude ?? [])])
|
||||
|
||||
if (gpx) {
|
||||
formData.append("gpx", gpx);
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
clearUndoRedoStack();
|
||||
|
||||
if ($formData.expand!.gpx_data) {
|
||||
$formData.id ??= cryptoRandomString({ length: 15 });
|
||||
const gpx = GPX.parse($formData.expand!.gpx_data);
|
||||
if (!(gpx instanceof Error)) {
|
||||
if (gpx.rte && !gpx.trk) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { get } from "svelte/store";
|
||||
|
||||
export const load: Load = async ({ params, fetch, url }) => {
|
||||
const user = get(currentUser)
|
||||
|
||||
|
||||
if (!params.id) {
|
||||
return error(400, "Bad Request")
|
||||
}
|
||||
@@ -17,7 +17,14 @@ export const load: Load = async ({ params, fetch, url }) => {
|
||||
|
||||
let trail: Trail;
|
||||
if (params.id === "new") {
|
||||
trail = new Trail("", { category: categories[0] });
|
||||
// duplicate trail
|
||||
if (url.searchParams.has("orig")) {
|
||||
const originalId = url.searchParams.get("orig")!;
|
||||
const originalTrail = await trails_show(originalId, undefined, undefined, true, fetch);
|
||||
trail = Trail.from(originalTrail)
|
||||
} else {
|
||||
trail = new Trail("", { category: categories[0] });
|
||||
}
|
||||
} else {
|
||||
trail = await trails_show(params.id, undefined, url.searchParams.get("share") ?? undefined, true, fetch);
|
||||
}
|
||||
|
||||
@@ -81,11 +81,11 @@
|
||||
localStorage.removeItem(TRAIL_LIST_FILTER_STORAGE_KEY);
|
||||
});
|
||||
|
||||
async function handleFilterUpdate() {
|
||||
async function handleFilterUpdate(resetPagination: boolean = true) {
|
||||
loading = true;
|
||||
persistFilter();
|
||||
|
||||
await paginate(1, pagination.items, false);
|
||||
await paginate(resetPagination ? 1 : pagination.page, pagination.items);
|
||||
|
||||
loading = false;
|
||||
}
|
||||
@@ -96,12 +96,14 @@
|
||||
try {
|
||||
await doPaginate(newPage, items);
|
||||
} catch (err: any) {
|
||||
let apiError : APIError = err;
|
||||
if (apiError.status == 413) { // content too large
|
||||
|
||||
let apiError: APIError = err;
|
||||
if (apiError.status == 413) {
|
||||
// content too large
|
||||
|
||||
let newItems = 10;
|
||||
|
||||
if (items == 12 || items == 24 || items == 48 || items == 96) { // cards view
|
||||
|
||||
if (items == 12 || items == 24 || items == 48 || items == 96) {
|
||||
// cards view
|
||||
if (items > 96) {
|
||||
newItems = 96;
|
||||
} else if (items > 48) {
|
||||
@@ -122,11 +124,11 @@
|
||||
newItems = 10;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await doPaginate(newPage, newItems);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
page.url.searchParams.set("page", newPage.toString());
|
||||
goto(`?${page.url.searchParams.toString()}`, { keepFocus: true, noScroll: !scrollToTop });
|
||||
}
|
||||
@@ -153,14 +155,14 @@
|
||||
categories={page.data.categories}
|
||||
bind:filter
|
||||
{filterExpanded}
|
||||
onupdate={handleFilterUpdate}
|
||||
onupdate={() => handleFilterUpdate()}
|
||||
></TrailFilterPanel>
|
||||
<TrailList
|
||||
bind:filter
|
||||
{loading}
|
||||
{trails}
|
||||
{pagination}
|
||||
onupdate={handleFilterUpdate}
|
||||
onupdate={() => handleFilterUpdate(false)}
|
||||
onpagination={paginate}
|
||||
></TrailList>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user