hammerhead integration added (#628)
* hammerhead integration added * typo corrected * start date added, optimize some translations * new option to send trail to hammerhead * fix elevation information (was in cm) * skip empty tracks on import * update docs * remove manual user-id input * fix merge issues * remove user-id from documentation * fix hammerhead logo for light theme * fix hammerhead logo for light theme --------- Co-authored-by: Christian Beutel <> Co-authored-by: Flomp <Flomp@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import Datepicker from "$lib/components/base/datepicker.svelte";
|
||||
import Modal from "$lib/components/base/modal.svelte";
|
||||
import TextField from "$lib/components/base/text_field.svelte";
|
||||
import Toggle from "$lib/components/base/toggle.svelte";
|
||||
import { HammerheadSchema } from "$lib/models/api/integration_schema";
|
||||
import type {
|
||||
Integration,
|
||||
HammerheadIntegration,
|
||||
} from "$lib/models/integration";
|
||||
import { validator } from "@felte/validator-zod";
|
||||
import { createForm } from "felte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
interface Props {
|
||||
integration?: Integration;
|
||||
onsave?: (hammerheadIntegration: HammerheadIntegration) => void;
|
||||
}
|
||||
|
||||
let { integration, onsave }: Props = $props();
|
||||
|
||||
let modal: Modal;
|
||||
|
||||
export function openModal() {
|
||||
errors.set({});
|
||||
modal.openModal();
|
||||
}
|
||||
|
||||
const getInitialFormValues = () => ({
|
||||
email: integration?.hammerhead?.email ?? "",
|
||||
password: integration?.hammerhead?.password ?? "",
|
||||
completed: integration?.hammerhead?.completed ?? true,
|
||||
planned: integration?.hammerhead?.planned ?? true,
|
||||
active: integration?.hammerhead?.active ?? false,
|
||||
after: integration?.hammerhead?.after,
|
||||
});
|
||||
|
||||
const {
|
||||
form,
|
||||
errors,
|
||||
data: formData,
|
||||
} = createForm({
|
||||
initialValues: getInitialFormValues(),
|
||||
extend: validator({
|
||||
schema: HammerheadSchema,
|
||||
}),
|
||||
onSubmit: async (form) => {
|
||||
form.active = integration?.hammerhead?.active ?? form.active;
|
||||
onsave?.(form);
|
||||
modal.closeModal();
|
||||
},
|
||||
});
|
||||
|
||||
function clearAfterDate() {
|
||||
($formData as any).after = undefined;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
id="hammerhead-settings-modal"
|
||||
size="md:max-w-lg"
|
||||
title={"Hammerhead " + $_("settings")}
|
||||
bind:this={modal}
|
||||
>
|
||||
{#snippet content()}
|
||||
<form id="hammerhead-settings-form" class="space-y-2" use:form>
|
||||
<TextField
|
||||
label={$_("email")}
|
||||
placeholder="user@example.com"
|
||||
name="email"
|
||||
error={$errors.email}
|
||||
></TextField>
|
||||
<TextField
|
||||
label={$_("password")}
|
||||
placeholder={integration?.hammerhead ? `(${$_("unchanged")})` : ""}
|
||||
name="password"
|
||||
type="password"
|
||||
error={$errors.password}
|
||||
></TextField>
|
||||
<div class="flex flex-wrap gap-x-4">
|
||||
<Toggle name="planned" label={$_("planned-tours", { values: { n: 2 } })}
|
||||
></Toggle>
|
||||
<Toggle
|
||||
name="completed"
|
||||
label={$_("completed-tours", { values: { n: 2 } })}
|
||||
></Toggle>
|
||||
</div>
|
||||
<p
|
||||
class="text-xs text-gray-500 max-w-lg pt-4 pb-1 border-t border-input-border"
|
||||
>
|
||||
{$_("hammerhead-integration-after-date-hint")}
|
||||
</p>
|
||||
<div class="flex items-end relative gap-x-2">
|
||||
<Datepicker
|
||||
error={$errors.after}
|
||||
label={$_("ignore-trails-before-date")}
|
||||
bind:value={$formData.after}
|
||||
></Datepicker>
|
||||
<button
|
||||
class="btn-icon mb-[10px]"
|
||||
type="button"
|
||||
onclick={clearAfterDate}
|
||||
aria-label="Clear 'after' date"
|
||||
><i class="fa fa-close"></i></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
{/snippet}
|
||||
{#snippet footer()}
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="btn-secondary" onclick={() => modal.closeModal()}
|
||||
>{$_("cancel")}</button
|
||||
>
|
||||
<button
|
||||
class="btn-primary"
|
||||
form="hammerhead-settings-form"
|
||||
type="submit"
|
||||
name="save">{$_("save")}</button
|
||||
>
|
||||
</div>
|
||||
{/snippet}</Modal
|
||||
>
|
||||
@@ -70,7 +70,7 @@
|
||||
<Modal
|
||||
id="strava-settings-modal"
|
||||
size="md:min-w-lg"
|
||||
title={"strava " + $_("settings")}
|
||||
title={"Strava " + $_("settings")}
|
||||
bind:this={modal}
|
||||
>
|
||||
{#snippet content()}
|
||||
@@ -114,7 +114,7 @@
|
||||
<div class="flex items-end relative gap-x-2 pt-2 border-t border-input-border">
|
||||
<Datepicker
|
||||
error={$errors.after}
|
||||
label={$_("after")}
|
||||
label={$_("ignore-trails-before-date")}
|
||||
bind:value={$formData.after}
|
||||
></Datepicker>
|
||||
<button
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
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 {
|
||||
integrations,
|
||||
integrations_index,
|
||||
uploadGpx,
|
||||
} from "$lib/stores/integration_store";
|
||||
import {
|
||||
lists_add_trail,
|
||||
lists_index,
|
||||
@@ -17,12 +21,14 @@
|
||||
import { trail2gpx } from "$lib/util/gpx_util";
|
||||
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
||||
import JSZip from "jszip";
|
||||
import type { Snippet } from "svelte";
|
||||
import { onMount, type Snippet } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { get } from "svelte/store";
|
||||
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 TrailSendModal from "./trail_send_modal.svelte";
|
||||
import TrailShareModal from "./trail_share_modal.svelte";
|
||||
|
||||
interface Props {
|
||||
@@ -40,8 +46,56 @@
|
||||
let listSelectModal: ListSearchModal;
|
||||
let trailExportModal: TrailExportModal;
|
||||
let trailShareModal: TrailShareModal;
|
||||
let trailSendModal: TrailSendModal;
|
||||
|
||||
const hammerheadIntegration = $derived(
|
||||
$integrations.find((integration) =>
|
||||
Boolean(integration.hammerhead?.active)
|
||||
)
|
||||
);
|
||||
|
||||
let lists: List[] = $state([]);
|
||||
let integrationsLoading = false;
|
||||
let integrationsLoadedForUser: string | undefined;
|
||||
|
||||
onMount(() => {
|
||||
const unsubscribe = currentUser.subscribe(async (user) => {
|
||||
if (!user) {
|
||||
integrationsLoadedForUser = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = get(integrations);
|
||||
if (
|
||||
existing.length &&
|
||||
existing[0]?.user === user.id
|
||||
) {
|
||||
integrationsLoadedForUser = user.id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
integrationsLoading ||
|
||||
integrationsLoadedForUser === user.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
integrationsLoading = true;
|
||||
try {
|
||||
await integrations_index();
|
||||
integrationsLoadedForUser = user.id;
|
||||
} catch (error) {
|
||||
console.error("Failed to load integrations", error);
|
||||
} finally {
|
||||
integrationsLoading = false;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
|
||||
let loading: boolean = $state(false);
|
||||
|
||||
@@ -212,6 +266,15 @@
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isMultiselectMode() && hammerheadIntegration && canExport()
|
||||
? [
|
||||
{
|
||||
text: $_("send-to"),
|
||||
value: "send-to",
|
||||
icon: "upload",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -336,6 +399,51 @@
|
||||
updateTrailsVisibility();
|
||||
} else if (ddVal == "delete") {
|
||||
confirmModal.openModal();
|
||||
} else if (item.value == "send-to") {
|
||||
trailSendModal.openModal();
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadToHammerhead() {
|
||||
if (!hammerheadIntegration || !hasTrail()) {
|
||||
console.error("No Hammerhead integration found.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const uTrail of trails!) {
|
||||
try {
|
||||
if (uTrail.gpx) {
|
||||
const gpxData = await trail2gpx(uTrail, $currentUser);
|
||||
const formData = new FormData();
|
||||
const gpxFile = new File(
|
||||
[gpxData],
|
||||
`${uTrail.name || "trail"}.gpx`,
|
||||
{ type: "application/gpx+xml" },
|
||||
);
|
||||
formData.append("file", gpxFile);
|
||||
|
||||
await uploadGpx("hammerhead", gpxFile);
|
||||
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: $_("uploaded-trail-to-hammerhead"),
|
||||
});
|
||||
} else {
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: $_("trail-has-no-gpx"),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: $_("error-uploading-trail-to-hammerhead"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,3 +725,11 @@
|
||||
onsave={handleShareUpdate}
|
||||
bind:this={trailShareModal}
|
||||
></TrailShareModal>
|
||||
<TrailSendModal
|
||||
bind:this={trailSendModal}
|
||||
onsend={async (settings) => {
|
||||
if (settings.integrationName === "hammerhead") {
|
||||
await uploadToHammerhead();
|
||||
}
|
||||
}}
|
||||
></TrailSendModal>
|
||||
|
||||
48
web/src/lib/components/trail/trail_send_modal.svelte
Normal file
48
web/src/lib/components/trail/trail_send_modal.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import Modal from "$lib/components/base/modal.svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import hammerheadLogoWhite from "$lib/assets/svgs/logos/hammerhead_white.svg";
|
||||
import hammerheadLogoDark from "$lib/assets/svgs/logos/hammerhead_dark.svg";
|
||||
import { theme } from "$lib/stores/theme_store";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
onsend?: (settings: typeof sendSettings) => void;
|
||||
}
|
||||
|
||||
let { title = $_("send-to"), onsend: onexport }: Props = $props();
|
||||
|
||||
let modal: Modal;
|
||||
|
||||
export function openModal() {
|
||||
modal.openModal();
|
||||
}
|
||||
|
||||
const sendSettings: {
|
||||
integrationName: string;
|
||||
} = $state({
|
||||
integrationName: "hammerhead",
|
||||
});
|
||||
|
||||
function sendTrail() {
|
||||
onexport?.(sendSettings);
|
||||
modal.closeModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal id="send-modal" {title} size="md:min-w-sm" bind:this={modal}>
|
||||
{#snippet content()}
|
||||
<div>
|
||||
<button class="btn-secondary" onclick={sendTrail}>
|
||||
<img class="h-20" src={$theme == "light" ? hammerheadLogoDark : hammerheadLogoWhite} alt="integration logo"/>
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet footer()}
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="btn-secondary" onclick={() => modal.closeModal()}
|
||||
>{$_("cancel")}</button
|
||||
>
|
||||
</div>
|
||||
{/snippet}</Modal
|
||||
>
|
||||
Reference in New Issue
Block a user