mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-07-30 15:28:30 -04:00
Add integration settings component and define built-in integrations
- Introduced a new `IntegrationsSettings` component for managing external service integrations. - Created `integrationCatalog.ts` to define built-in integrations with their respective metadata. - Updated English locale file to include translations for built-in services and their descriptions. - Refactored settings page to incorporate the new integrations component, enhancing user experience for managing integrations.
This commit is contained in:
510
frontend/src/lib/components/settings/IntegrationsSettings.svelte
Normal file
510
frontend/src/lib/components/settings/IntegrationsSettings.svelte
Normal file
@@ -0,0 +1,510 @@
|
||||
<script lang="ts">
|
||||
import { t } from 'svelte-i18n';
|
||||
import { addToast } from '$lib/toasts';
|
||||
import type { ImmichIntegration, User, WandererIntegration } from '$lib/types';
|
||||
import ImmichLogo from '$lib/assets/immich.svg';
|
||||
import GoogleMapsLogo from '$lib/assets/google_maps.svg';
|
||||
import StravaLogo from '$lib/assets/strava.svg';
|
||||
import WandererLogoSrc from '$lib/assets/wanderer.svg';
|
||||
import { BUILTIN_INTEGRATIONS } from './integrationCatalog';
|
||||
import MapIcon from '~icons/mdi/map';
|
||||
import BookOpenPageVariant from '~icons/mdi/book-open-page-variant';
|
||||
import WeatherSunset from '~icons/mdi/weather-sunset';
|
||||
import MapSearch from '~icons/mdi/map-search';
|
||||
|
||||
export let user: User;
|
||||
export let immichIntegration: ImmichIntegration | null = null;
|
||||
export let googleMapsEnabled = false;
|
||||
export let stravaGlobalEnabled = false;
|
||||
export let stravaUserEnabled = false;
|
||||
export let wandererEnabled = false;
|
||||
export let wandererIntegration: WandererIntegration | null = null;
|
||||
|
||||
let newImmichIntegration: ImmichIntegration = {
|
||||
server_url: '',
|
||||
api_key: '',
|
||||
id: '',
|
||||
copy_locally: true
|
||||
};
|
||||
|
||||
let newWandererIntegration: WandererIntegration = {
|
||||
server_url: '',
|
||||
api_key: '',
|
||||
id: ''
|
||||
};
|
||||
|
||||
const builtinIcons = {
|
||||
osm: MapIcon,
|
||||
wikipedia: BookOpenPageVariant,
|
||||
sunrise: WeatherSunset,
|
||||
overpass: MapSearch
|
||||
} as const;
|
||||
|
||||
function handleImmichError(data: {
|
||||
code?: string;
|
||||
details?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
server_url?: string[];
|
||||
api_key?: string[];
|
||||
}) {
|
||||
if (data.code === 'immich.connection_failed') {
|
||||
return `${$t('immich.connection_error')}: ${data.details || data.message}`;
|
||||
}
|
||||
if (data.code === 'immich.integration_exists') {
|
||||
return $t('immich.integration_already_exists');
|
||||
}
|
||||
if (data.code === 'immich.integration_not_found') {
|
||||
return $t('immich.integration_not_found');
|
||||
}
|
||||
if (data.error && data.message) {
|
||||
return data.message;
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
if (data.server_url) errors.push(`Server URL: ${data.server_url.join(', ')}`);
|
||||
if (data.api_key) errors.push(`API Key: ${data.api_key.join(', ')}`);
|
||||
return errors.length > 0
|
||||
? `${$t('immich.validation_error')}: ${errors.join('; ')}`
|
||||
: $t('immich.immich_error');
|
||||
}
|
||||
|
||||
async function enableImmichIntegration() {
|
||||
const isUpdate = !!immichIntegration?.id;
|
||||
const url = isUpdate
|
||||
? `/api/integrations/immich/${immichIntegration?.id ?? ''}/`
|
||||
: '/api/integrations/immich/';
|
||||
const method = isUpdate ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newImmichIntegration)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
addToast('success', $t(isUpdate ? 'immich.immich_updated' : 'immich.immich_enabled'));
|
||||
immichIntegration = data;
|
||||
newImmichIntegration = { server_url: '', api_key: '', id: '', copy_locally: true };
|
||||
} else {
|
||||
addToast('error', handleImmichError(data));
|
||||
}
|
||||
} catch {
|
||||
addToast('error', $t('immich.network_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function disableImmichIntegration() {
|
||||
if (!immichIntegration?.id) return;
|
||||
|
||||
const res = await fetch(`/api/integrations/immich/${immichIntegration.id}/`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('immich.immich_disabled'));
|
||||
immichIntegration = null;
|
||||
} else {
|
||||
addToast('error', $t('immich.immich_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function stravaAuthorizeRedirect() {
|
||||
const res = await fetch('/api/integrations/strava/authorize/', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
window.location.href = data.auth_url;
|
||||
} else {
|
||||
addToast('error', $t('strava.authorization_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function stravaDisconnect() {
|
||||
const res = await fetch('/api/integrations/strava/disable/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('strava.disconnected'));
|
||||
stravaUserEnabled = false;
|
||||
} else {
|
||||
addToast('error', $t('strava.disconnect_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function wandererDisconnect() {
|
||||
const res = await fetch('/api/integrations/wanderer/disable/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('wanderer.disconnected'));
|
||||
wandererEnabled = false;
|
||||
wandererIntegration = null;
|
||||
} else {
|
||||
addToast('error', $t('wanderer.disconnect_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function enableWandererIntegration() {
|
||||
const integrationId = newWandererIntegration.id || wandererIntegration?.id;
|
||||
const isUpdate = !!integrationId;
|
||||
const url = isUpdate
|
||||
? `/api/integrations/wanderer/${integrationId}/`
|
||||
: '/api/integrations/wanderer/';
|
||||
const method = isUpdate ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newWandererIntegration)
|
||||
});
|
||||
const responseData = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
addToast('success', $t(isUpdate ? 'wanderer.updated' : 'wanderer.connected'));
|
||||
wandererIntegration = responseData;
|
||||
wandererEnabled = true;
|
||||
newWandererIntegration = { server_url: '', api_key: '', id: '' };
|
||||
} else {
|
||||
const message =
|
||||
responseData.error ||
|
||||
responseData.detail ||
|
||||
(Array.isArray(responseData) ? responseData.join(', ') : null) ||
|
||||
$t('wanderer.connection_error');
|
||||
addToast('error', message);
|
||||
}
|
||||
} catch {
|
||||
addToast('error', $t('wanderer.connection_error'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Immich -->
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<img src={ImmichLogo} alt="Immich" class="w-8 h-8 shrink-0" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-xl font-bold">Immich</h3>
|
||||
<p class="text-sm text-base-content/70">{$t('immich.immich_integration_desc')}</p>
|
||||
</div>
|
||||
{#if immichIntegration}
|
||||
<div class="badge badge-success ml-auto shrink-0">{$t('settings.connected')}</div>
|
||||
{:else}
|
||||
<div class="badge badge-error ml-auto shrink-0">{$t('settings.disconnected')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if immichIntegration && !newImmichIntegration.id}
|
||||
<div class="flex gap-4 justify-center mb-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning"
|
||||
on:click={() => {
|
||||
if (immichIntegration) newImmichIntegration = { ...immichIntegration, api_key: '' };
|
||||
}}
|
||||
>
|
||||
✏️ {$t('lodging.edit')}
|
||||
</button>
|
||||
<button type="button" class="btn btn-error" on:click={disableImmichIntegration}>
|
||||
❌ {$t('immich.disable')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !immichIntegration || newImmichIntegration.id}
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="immich-server-url">
|
||||
<span class="label-text font-medium">{$t('immich.server_url')}</span>
|
||||
</label>
|
||||
<input
|
||||
id="immich-server-url"
|
||||
type="url"
|
||||
bind:value={newImmichIntegration.server_url}
|
||||
class="input input-bordered input-primary focus:input-primary w-full"
|
||||
placeholder="https://immich.example.com/api"
|
||||
/>
|
||||
{#if newImmichIntegration.server_url && !newImmichIntegration.server_url.endsWith('api')}
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning">{$t('immich.api_note')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if newImmichIntegration.server_url && (newImmichIntegration.server_url.includes('localhost') || newImmichIntegration.server_url.includes('127.0.0.1'))}
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning">{$t('immich.localhost_note')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="immich-api-key">
|
||||
<span class="label-text font-medium">{$t('immich.api_key')}</span>
|
||||
</label>
|
||||
<input
|
||||
id="immich-api-key"
|
||||
type="password"
|
||||
bind:value={newImmichIntegration.api_key}
|
||||
class="input input-bordered input-primary focus:input-primary w-full"
|
||||
placeholder={$t('immich.api_key_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={newImmichIntegration.copy_locally}
|
||||
class="toggle toggle-primary"
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">
|
||||
{$t('immich.copy_locally') || 'Copy Locally'}
|
||||
</span>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{$t('immich.copy_locally_desc') || 'If enabled, files will be copied locally.'}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="button" on:click={enableImmichIntegration} class="btn btn-primary w-full">
|
||||
{!immichIntegration?.id
|
||||
? `🔗 ${$t('immich.enable_integration')}`
|
||||
: `💾 ${$t('immich.update_integration')}`}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 p-4 bg-info/10 rounded-lg">
|
||||
<p class="text-sm">
|
||||
📖 {$t('immich.need_help')}
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/immich_integration.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{$t('navbar.documentation')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Google Maps -->
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<img src={GoogleMapsLogo} alt="Google Maps" class="w-8 h-8 shrink-0" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-xl font-bold">Google Maps</h3>
|
||||
<p class="text-sm text-base-content/70">{$t('google_maps.google_maps_integration_desc')}</p>
|
||||
</div>
|
||||
{#if googleMapsEnabled}
|
||||
<div class="badge badge-success ml-auto shrink-0">{$t('settings.connected')}</div>
|
||||
{:else}
|
||||
<div class="badge badge-error ml-auto shrink-0">{$t('settings.disconnected')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if user.is_staff || !googleMapsEnabled}
|
||||
<div class="mt-4 p-4 bg-info/10 rounded-lg">
|
||||
{#if user.is_staff}
|
||||
<p class="text-sm">
|
||||
📖 {$t('immich.need_help')}
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/google_maps_integration.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{$t('navbar.documentation')}
|
||||
</a>
|
||||
</p>
|
||||
{:else if !googleMapsEnabled}
|
||||
<p class="text-sm">ℹ️ {$t('google_maps.google_maps_integration_desc_no_staff')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Strava -->
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<img src={StravaLogo} alt="Strava" class="w-8 h-8 rounded-md shrink-0" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-xl font-bold">Strava</h3>
|
||||
<p class="text-sm text-base-content/70">{$t('strava.strava_integration_desc')}</p>
|
||||
</div>
|
||||
{#if stravaGlobalEnabled && stravaUserEnabled}
|
||||
<div class="badge badge-success ml-auto shrink-0">{$t('settings.connected')}</div>
|
||||
{:else}
|
||||
<div class="badge badge-error ml-auto shrink-0">{$t('settings.disconnected')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !stravaGlobalEnabled}
|
||||
<div class="text-center">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{$t('strava.not_enabled') || 'Strava integration is not enabled on this instance.'}
|
||||
</p>
|
||||
</div>
|
||||
{:else if !stravaUserEnabled}
|
||||
<div class="text-center">
|
||||
<button type="button" class="btn btn-primary" on:click={stravaAuthorizeRedirect}>
|
||||
🔗 {$t('strava.connect_account')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center">
|
||||
<button type="button" class="btn btn-error" on:click={stravaDisconnect}>
|
||||
❌ {$t('strava.disconnect')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if user.is_staff || !stravaGlobalEnabled}
|
||||
<div class="mt-4 p-4 bg-info/10 rounded-lg">
|
||||
{#if user.is_staff}
|
||||
<p class="text-sm">
|
||||
📖 {$t('immich.need_help')}
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/strava_integration.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{$t('navbar.documentation')}
|
||||
</a>
|
||||
</p>
|
||||
{:else if !stravaGlobalEnabled}
|
||||
<p class="text-sm">ℹ️ {$t('google_maps.google_maps_integration_desc_no_staff')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Wanderer -->
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<img src={WandererLogoSrc} alt="Wanderer" class="w-8 h-8 shrink-0" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-xl font-bold">Wanderer</h3>
|
||||
<p class="text-sm text-base-content/70">{$t('wanderer.wanderer_integration_desc')}</p>
|
||||
</div>
|
||||
{#if wandererEnabled}
|
||||
<div class="badge badge-success ml-auto shrink-0">{$t('settings.connected')}</div>
|
||||
{:else}
|
||||
<div class="badge badge-error ml-auto shrink-0">{$t('settings.disconnected')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if wandererIntegration && !newWandererIntegration.id}
|
||||
<div class="flex gap-4 justify-center mb-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning"
|
||||
on:click={() => {
|
||||
if (wandererIntegration) {
|
||||
newWandererIntegration = { ...wandererIntegration, api_key: '' };
|
||||
}
|
||||
}}
|
||||
>
|
||||
✏️ {$t('lodging.edit')}
|
||||
</button>
|
||||
<button type="button" class="btn btn-error" on:click={wandererDisconnect}>
|
||||
❌ {$t('strava.disconnect')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !wandererIntegration || newWandererIntegration.id}
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="wanderer-server-url">
|
||||
<span class="label-text font-medium">{$t('wanderer.server_url')}</span>
|
||||
</label>
|
||||
<input
|
||||
id="wanderer-server-url"
|
||||
type="url"
|
||||
class="input input-bordered input-primary focus:input-primary w-full"
|
||||
placeholder="https://wanderer.example.com"
|
||||
bind:value={newWandererIntegration.server_url}
|
||||
/>
|
||||
{#if newWandererIntegration.server_url && (newWandererIntegration.server_url.includes('localhost') || newWandererIntegration.server_url.includes('127.0.0.1'))}
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning">{$t('wanderer.localhost_note')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="wanderer-api-key">
|
||||
<span class="label-text font-medium">{$t('wanderer.api_key')}</span>
|
||||
</label>
|
||||
<input
|
||||
id="wanderer-api-key"
|
||||
type="password"
|
||||
class="input input-bordered input-primary focus:input-primary w-full"
|
||||
placeholder={$t('wanderer.api_key_placeholder')}
|
||||
bind:value={newWandererIntegration.api_key}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary w-full" on:click={enableWandererIntegration}>
|
||||
{!wandererIntegration?.id
|
||||
? `🔗 ${$t('adventures.connect_to_wanderer')}`
|
||||
: `💾 ${$t('wanderer.update_integration')}`}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 p-4 bg-info/10 rounded-lg">
|
||||
<p class="text-sm">
|
||||
📖 {$t('immich.need_help')}
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/wanderer_integration.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{$t('navbar.documentation')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Included by default -->
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="p-2 bg-base-100 rounded-lg text-xl leading-none">✨</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-xl font-bold">{$t('settings.integrations_hub.builtin_title')}</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{$t('settings.integrations_hub.builtin_desc')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="badge badge-success ml-auto shrink-0">
|
||||
{$t('settings.integrations_hub.included')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-base-300 rounded-lg border border-base-300 bg-base-100/60">
|
||||
{#each BUILTIN_INTEGRATIONS as integration (integration.id)}
|
||||
{@const Icon = builtinIcons[integration.icon]}
|
||||
<li class="flex items-start gap-3 p-4">
|
||||
<div class="p-1.5 text-primary shrink-0">
|
||||
<Icon class="w-5 h-5" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-medium">{$t(integration.nameKey)}</p>
|
||||
<p class="text-sm text-base-content/70 mt-0.5">{$t(integration.descriptionKey)}</p>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
33
frontend/src/lib/components/settings/integrationCatalog.ts
Normal file
33
frontend/src/lib/components/settings/integrationCatalog.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export type BuiltinIntegrationDef = {
|
||||
id: string;
|
||||
nameKey: string;
|
||||
descriptionKey: string;
|
||||
icon: 'osm' | 'wikipedia' | 'sunrise' | 'overpass';
|
||||
};
|
||||
|
||||
export const BUILTIN_INTEGRATIONS: BuiltinIntegrationDef[] = [
|
||||
{
|
||||
id: 'nominatim',
|
||||
nameKey: 'settings.integrations_hub.builtin.nominatim_name',
|
||||
descriptionKey: 'settings.integrations_hub.builtin.nominatim_desc',
|
||||
icon: 'osm'
|
||||
},
|
||||
{
|
||||
id: 'wikipedia',
|
||||
nameKey: 'settings.integrations_hub.builtin.wikipedia_name',
|
||||
descriptionKey: 'settings.integrations_hub.builtin.wikipedia_desc',
|
||||
icon: 'wikipedia'
|
||||
},
|
||||
{
|
||||
id: 'sunrise_sunset',
|
||||
nameKey: 'settings.integrations_hub.builtin.sunrise_name',
|
||||
descriptionKey: 'settings.integrations_hub.builtin.sunrise_desc',
|
||||
icon: 'sunrise'
|
||||
},
|
||||
{
|
||||
id: 'overpass',
|
||||
nameKey: 'settings.integrations_hub.builtin.overpass_name',
|
||||
descriptionKey: 'settings.integrations_hub.builtin.overpass_desc',
|
||||
icon: 'overpass'
|
||||
}
|
||||
];
|
||||
@@ -714,6 +714,38 @@
|
||||
"emails": "Emails",
|
||||
"integrations": "Integrations",
|
||||
"integrations_desc": "Connect external services to enhance your experience",
|
||||
"integrations_hub": {
|
||||
"summary_builtin": "Built-in services",
|
||||
"summary_builtin_desc": "Always available on every instance",
|
||||
"summary_instance": "Instance services",
|
||||
"summary_instance_desc": "Enabled by your server admin",
|
||||
"summary_personal": "Your connections",
|
||||
"summary_personal_desc": "Linked to your account",
|
||||
"builtin_title": "Built-in services",
|
||||
"builtin_desc": "These data sources power AdventureLog out of the box. No setup is required.",
|
||||
"included": "Included",
|
||||
"learn_more": "Learn more",
|
||||
"instance_title": "Instance services",
|
||||
"instance_desc": "Optional services configured by your AdventureLog administrator.",
|
||||
"personal_title": "Your connections",
|
||||
"personal_desc": "Link your own accounts and self-hosted services.",
|
||||
"google_maps_features": "Place search, map details, quick-add enrichment, and nearby recommendations.",
|
||||
"google_maps_active_hint": "Google Maps is active on this instance. Place search and enrichment use it when available.",
|
||||
"builtin": {
|
||||
"nominatim_name": "OpenStreetMap Nominatim",
|
||||
"nominatim_desc": "Fallback geocoding and place search when Google Maps is unavailable.",
|
||||
"nominatim_features": "Used for location search, reverse geocoding, and coordinate lookups.",
|
||||
"wikipedia_name": "Wikipedia",
|
||||
"wikipedia_desc": "Enriches locations with background descriptions and reference images.",
|
||||
"wikipedia_features": "Used when generating descriptions and importing Wikipedia photos.",
|
||||
"sunrise_name": "SunriseSunset.io",
|
||||
"sunrise_desc": "Adds sunrise and sunset times to your location visits.",
|
||||
"sunrise_features": "Shown on location detail pages for each visit date.",
|
||||
"overpass_name": "OpenStreetMap Overpass",
|
||||
"overpass_desc": "Powers nearby recommendations from OpenStreetMap data.",
|
||||
"overpass_features": "Used for map recommendations and as a fallback when Google is unavailable."
|
||||
}
|
||||
},
|
||||
"admin": "Admin",
|
||||
"advanced": "Advanced",
|
||||
"profile_info": "Profile Information",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Collection, ContentImage, Location, Collaborator, Lodging } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
@@ -49,8 +48,14 @@
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
// Handle both 'collection' and 'adventure' properties for backward compatibility
|
||||
let collection: Collection = (data.props as any).collection || (data.props as any).adventure;
|
||||
function getCollectionFromPageData(pageData: PageData): Collection | null | undefined {
|
||||
return (
|
||||
(pageData.props as { collection?: Collection; adventure?: Collection }).collection ??
|
||||
(pageData.props as { collection?: Collection; adventure?: Collection }).adventure
|
||||
);
|
||||
}
|
||||
|
||||
let collection: Collection | undefined;
|
||||
let currentSlide = 0;
|
||||
let notFound: boolean = false;
|
||||
let isLocationModalOpen: boolean = false;
|
||||
@@ -475,11 +480,40 @@
|
||||
selectedCalendarEvent = null;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!collection) {
|
||||
function resetCollectionPageUiState() {
|
||||
currentSlide = 0;
|
||||
isLocationModalOpen = false;
|
||||
isLodgingModalOpen = false;
|
||||
isTransportationModalOpen = false;
|
||||
isChecklistModalOpen = false;
|
||||
isNoteModalOpen = false;
|
||||
adventureToEdit = null;
|
||||
transportationToEdit = null;
|
||||
noteToEdit = null;
|
||||
checklistToEdit = null;
|
||||
lodgingToEdit = null;
|
||||
isImageModalOpen = false;
|
||||
isLocationLinkModalOpen = false;
|
||||
showCalendarModal = false;
|
||||
selectedCalendarEvent = null;
|
||||
isSocialShareModalOpen = false;
|
||||
calendarInitialDate = null;
|
||||
}
|
||||
|
||||
function applyCollectionPageData(collectionData: Collection | null | undefined) {
|
||||
if (!collectionData) {
|
||||
notFound = true;
|
||||
collection = undefined;
|
||||
resetCollectionPageUiState();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
notFound = false;
|
||||
collection = collectionData;
|
||||
resetCollectionPageUiState();
|
||||
}
|
||||
|
||||
$: applyCollectionPageData(getCollectionFromPageData(data));
|
||||
|
||||
function goToSlide(index: number) {
|
||||
currentSlide = index;
|
||||
|
||||
@@ -17,13 +17,9 @@
|
||||
import TotpModal from '$lib/components/TOTPModal.svelte';
|
||||
import { copyrightYear } from '$lib/config.js';
|
||||
import AppVersionDisplay from '$lib/components/shared/AppVersionDisplay.svelte';
|
||||
import ImmichLogo from '$lib/assets/immich.svg';
|
||||
import GoogleMapsLogo from '$lib/assets/google_maps.svg';
|
||||
import StravaLogo from '$lib/assets/strava.svg';
|
||||
import WandererLogoSrc from '$lib/assets/wanderer.svg';
|
||||
import IntegrationsSettings from '$lib/components/settings/IntegrationsSettings.svelte';
|
||||
|
||||
export let data;
|
||||
console.log(data);
|
||||
let user: User;
|
||||
let emails: typeof data.props.emails;
|
||||
if (data.user) {
|
||||
@@ -70,19 +66,6 @@
|
||||
|
||||
// Indicates restore operation in progress to disable button and show loader
|
||||
let isRestoring: boolean = false;
|
||||
let newImmichIntegration: ImmichIntegration = {
|
||||
server_url: '',
|
||||
api_key: '',
|
||||
id: '',
|
||||
copy_locally: true
|
||||
};
|
||||
|
||||
let newWandererIntegration: WandererIntegration = {
|
||||
server_url: '',
|
||||
api_key: '',
|
||||
id: ''
|
||||
};
|
||||
|
||||
let isMFAModalOpen: boolean = false;
|
||||
|
||||
let apiKeys: APIKey[] = data.props.apiKeys ?? [];
|
||||
@@ -345,74 +328,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleImmichError(data: {
|
||||
code: string;
|
||||
details: any;
|
||||
message: any;
|
||||
error: any;
|
||||
server_url: any[];
|
||||
api_key: any[];
|
||||
}) {
|
||||
if (data.code === 'immich.connection_failed') {
|
||||
return `${$t('immich.connection_error')}: ${data.details || data.message}`;
|
||||
} else if (data.code === 'immich.integration_exists') {
|
||||
return $t('immich.integration_already_exists');
|
||||
} else if (data.code === 'immich.integration_not_found') {
|
||||
return $t('immich.integration_not_found');
|
||||
} else if (data.error && data.message) {
|
||||
return data.message;
|
||||
} else {
|
||||
// Handle validation errors
|
||||
const errors = [];
|
||||
if (data.server_url) errors.push(`Server URL: ${data.server_url.join(', ')}`);
|
||||
if (data.api_key) errors.push(`API Key: ${data.api_key.join(', ')}`);
|
||||
return errors.length > 0
|
||||
? `${$t('immich.validation_error')}: ${errors.join('; ')}`
|
||||
: $t('immich.immich_error');
|
||||
}
|
||||
}
|
||||
|
||||
async function enableImmichIntegration() {
|
||||
const isUpdate = !!immichIntegration?.id;
|
||||
const url = isUpdate
|
||||
? `/api/integrations/immich/${immichIntegration?.id ?? ''}/`
|
||||
: '/api/integrations/immich/';
|
||||
const method = isUpdate ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newImmichIntegration)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
addToast('success', $t(isUpdate ? 'immich.immich_updated' : 'immich.immich_enabled'));
|
||||
immichIntegration = data;
|
||||
} else {
|
||||
addToast('error', handleImmichError(data));
|
||||
}
|
||||
} catch (error) {
|
||||
addToast('error', $t('immich.network_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function disableImmichIntegration() {
|
||||
if (immichIntegration && immichIntegration.id) {
|
||||
let res = await fetch(`/api/integrations/immich/${immichIntegration.id}/`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('immich.immich_disabled'));
|
||||
immichIntegration = null;
|
||||
} else {
|
||||
addToast('error', $t('immich.immich_error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function disableMfa() {
|
||||
const res = await fetch('/auth/browser/v1/account/authenticators/totp', {
|
||||
method: 'DELETE'
|
||||
@@ -428,86 +343,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function stravaAuthorizeRedirect() {
|
||||
const res = await fetch('/api/integrations/strava/authorize/', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
window.location.href = data.auth_url;
|
||||
} else {
|
||||
addToast('error', $t('strava.authorization_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function stravaDisconnect() {
|
||||
const res = await fetch('/api/integrations/strava/disable/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('strava.disconnected'));
|
||||
stravaUserEnabled = false;
|
||||
} else {
|
||||
addToast('error', $t('strava.disconnect_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function wandererDisconnect() {
|
||||
const res = await fetch('/api/integrations/wanderer/disable/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', $t('wanderer.disconnected'));
|
||||
wandererEnabled = false;
|
||||
wandererIntegration = null;
|
||||
} else {
|
||||
addToast('error', $t('wanderer.disconnect_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function enableWandererIntegration() {
|
||||
const integrationId = newWandererIntegration.id || wandererIntegration?.id;
|
||||
const isUpdate = !!integrationId;
|
||||
const url = isUpdate
|
||||
? `/api/integrations/wanderer/${integrationId}/`
|
||||
: '/api/integrations/wanderer/';
|
||||
const method = isUpdate ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newWandererIntegration)
|
||||
});
|
||||
const responseData = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
addToast('success', $t(isUpdate ? 'wanderer.updated' : 'wanderer.connected'));
|
||||
wandererIntegration = responseData;
|
||||
wandererEnabled = true;
|
||||
newWandererIntegration = { server_url: '', api_key: '', id: '' };
|
||||
} else {
|
||||
const message =
|
||||
responseData.error ||
|
||||
responseData.detail ||
|
||||
(Array.isArray(responseData) ? responseData.join(', ') : null) ||
|
||||
$t('wanderer.connection_error');
|
||||
addToast('error', message);
|
||||
}
|
||||
} catch {
|
||||
addToast('error', $t('wanderer.connection_error'));
|
||||
}
|
||||
}
|
||||
|
||||
async function createApiKey() {
|
||||
if (!newApiKeyName.trim()) return;
|
||||
const res = await fetch('/auth/api-keys/', {
|
||||
@@ -1339,309 +1174,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Immich Integration -->
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<img src={ImmichLogo} alt="Immich" class="w-8 h-8" />
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Immich</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{$t('immich.immich_integration_desc')}
|
||||
</p>
|
||||
</div>
|
||||
{#if immichIntegration}
|
||||
<div class="badge badge-success ml-auto">{$t('settings.connected')}</div>
|
||||
{:else}
|
||||
<div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if immichIntegration && !newImmichIntegration.id}
|
||||
<div class="flex gap-4 justify-center mb-4">
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
on:click={() => {
|
||||
if (immichIntegration) newImmichIntegration = immichIntegration;
|
||||
}}
|
||||
>
|
||||
✏️ {$t('lodging.edit')}
|
||||
</button>
|
||||
<button class="btn btn-error" on:click={disableImmichIntegration}>
|
||||
❌ {$t('immich.disable')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !immichIntegration || newImmichIntegration.id}
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">{$t('immich.server_url')}</span>
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={newImmichIntegration.server_url}
|
||||
class="input input-bordered input-primary focus:input-primary"
|
||||
placeholder="https://immich.example.com/api"
|
||||
/>
|
||||
{#if newImmichIntegration.server_url && !newImmichIntegration.server_url.endsWith('api')}
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning">{$t('immich.api_note')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if newImmichIntegration.server_url && (newImmichIntegration.server_url.indexOf('localhost') !== -1 || newImmichIntegration.server_url.indexOf('127.0.0.1') !== -1)}
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning"
|
||||
>{$t('immich.localhost_note')}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">{$t('immich.api_key')}</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={newImmichIntegration.api_key}
|
||||
class="input input-bordered input-primary focus:input-primary"
|
||||
placeholder={$t('immich.api_key_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Toggle for copy_locally -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={newImmichIntegration.copy_locally}
|
||||
class="toggle toggle-primary"
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium">
|
||||
{$t('immich.copy_locally') || 'Copy Locally'}
|
||||
</span>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{$t('immich.copy_locally_desc') ||
|
||||
'If enabled, files will be copied locally.'}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button on:click={enableImmichIntegration} class="btn btn-primary w-full">
|
||||
{!immichIntegration?.id
|
||||
? `🔗 ${$t('immich.enable_integration')}`
|
||||
: `💾 ${$t('immich.update_integration')}`}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 p-4 bg-info/10 rounded-lg">
|
||||
<p class="text-sm">
|
||||
📖 {$t('immich.need_help')}
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/immich_integration.html"
|
||||
target="_blank">{$t('navbar.documentation')}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Google maps integration - displayt only if its connected -->
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<img src={GoogleMapsLogo} alt="Google Maps" class="w-8 h-8" />
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Google Maps</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{$t('google_maps.google_maps_integration_desc')}
|
||||
</p>
|
||||
</div>
|
||||
{#if googleMapsEnabled}
|
||||
<div class="badge badge-success ml-auto">{$t('settings.connected')}</div>
|
||||
{:else}
|
||||
<div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if user.is_staff || !googleMapsEnabled}
|
||||
<div class="mt-4 p-4 bg-info/10 rounded-lg">
|
||||
{#if user.is_staff}
|
||||
<p class="text-sm">
|
||||
📖 {$t('immich.need_help')}
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/google_maps_integration.html"
|
||||
target="_blank">{$t('navbar.documentation')}</a
|
||||
>
|
||||
</p>
|
||||
{:else if !googleMapsEnabled}
|
||||
<p class="text-sm">
|
||||
ℹ️ {$t('google_maps.google_maps_integration_desc_no_staff')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Strava Integration Section -->
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<img src={StravaLogo} alt="Strava" class="w-8 h-8 rounded-md" />
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Strava</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{$t('strava.strava_integration_desc')}
|
||||
</p>
|
||||
</div>
|
||||
{#if stravaGlobalEnabled && stravaUserEnabled}
|
||||
<div class="badge badge-success ml-auto">{$t('settings.connected')}</div>
|
||||
{:else}
|
||||
<div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Content based on integration status -->
|
||||
{#if !stravaGlobalEnabled}
|
||||
<!-- Strava not enabled globally -->
|
||||
<div class="text-center">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{$t('strava.not_enabled') ||
|
||||
'Strava integration is not enabled on this instance.'}
|
||||
</p>
|
||||
</div>
|
||||
{:else if !stravaUserEnabled && stravaGlobalEnabled}
|
||||
<!-- Globally enabled but user not connected -->
|
||||
<div class="text-center">
|
||||
<button class="btn btn-primary" on:click={stravaAuthorizeRedirect}>
|
||||
🔗 {$t('strava.connect_account')}
|
||||
</button>
|
||||
</div>
|
||||
{:else if stravaGlobalEnabled && stravaUserEnabled}
|
||||
<!-- User connected - show management options -->
|
||||
<div class="text-center">
|
||||
<button class="btn btn-error" on:click={stravaDisconnect}>
|
||||
❌ {$t('strava.disconnect')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Help documentation link -->
|
||||
{#if user.is_staff || !stravaGlobalEnabled}
|
||||
<div class="mt-4 p-4 bg-info/10 rounded-lg">
|
||||
{#if user.is_staff}
|
||||
<p class="text-sm">
|
||||
📖 {$t('immich.need_help')}
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/strava_integration.html"
|
||||
target="_blank">{$t('navbar.documentation')}</a
|
||||
>
|
||||
</p>
|
||||
{:else if !stravaGlobalEnabled}
|
||||
<p class="text-sm">
|
||||
ℹ️ {$t('google_maps.google_maps_integration_desc_no_staff')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-base-200 rounded-xl mb-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<img src={WandererLogoSrc} alt="Wanderer" class="w-8 h-8" />
|
||||
<div>
|
||||
<h3 class="text-xl font-bold">Wanderer</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{$t('wanderer.wanderer_integration_desc')}
|
||||
</p>
|
||||
</div>
|
||||
{#if wandererEnabled}
|
||||
<div class="badge badge-success ml-auto">{$t('settings.connected')}</div>
|
||||
{:else}
|
||||
<div class="badge badge-error ml-auto">{$t('settings.disconnected')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if wandererIntegration && !newWandererIntegration.id}
|
||||
<div class="flex gap-4 justify-center mb-4">
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
on:click={() => {
|
||||
if (wandererIntegration) {
|
||||
newWandererIntegration = {
|
||||
...wandererIntegration,
|
||||
api_key: ''
|
||||
};
|
||||
}
|
||||
}}
|
||||
>
|
||||
✏️ {$t('lodging.edit')}
|
||||
</button>
|
||||
<button class="btn btn-error" on:click={wandererDisconnect}>
|
||||
❌ {$t('strava.disconnect')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !wandererIntegration || newWandererIntegration.id}
|
||||
<div class="space-y-4">
|
||||
<div class="form-control">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">{$t('wanderer.server_url')}</span>
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
class="input input-bordered input-primary focus:input-primary"
|
||||
placeholder="https://wanderer.example.com"
|
||||
bind:value={newWandererIntegration.server_url}
|
||||
/>
|
||||
{#if newWandererIntegration.server_url && (newWandererIntegration.server_url.indexOf('localhost') !== -1 || newWandererIntegration.server_url.indexOf('127.0.0.1') !== -1)}
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-warning"
|
||||
>{$t('wanderer.localhost_note')}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">{$t('wanderer.api_key')}</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered input-primary focus:input-primary"
|
||||
placeholder={$t('wanderer.api_key_placeholder')}
|
||||
bind:value={newWandererIntegration.api_key}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-full" on:click={enableWandererIntegration}>
|
||||
{!wandererIntegration?.id
|
||||
? `🔗 ${$t('adventures.connect_to_wanderer')}`
|
||||
: `💾 ${$t('wanderer.update_integration')}`}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 p-4 bg-info/10 rounded-lg">
|
||||
<p class="text-sm">
|
||||
📖 {$t('immich.need_help')}
|
||||
<a
|
||||
class="link link-primary"
|
||||
href="https://adventurelog.app/docs/configuration/wanderer_integration.html"
|
||||
target="_blank">{$t('navbar.documentation')}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<IntegrationsSettings
|
||||
{user}
|
||||
bind:immichIntegration
|
||||
bind:googleMapsEnabled
|
||||
bind:stravaGlobalEnabled
|
||||
bind:stravaUserEnabled
|
||||
bind:wandererEnabled
|
||||
bind:wandererIntegration
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user