mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-20 21:39:37 +02:00
feat: implement Immich integration with CRUD API, add serializer, and enhance frontend components
This commit is contained in:
parent
5d12d103fc
commit
cee9f16cf5
12 changed files with 317 additions and 8 deletions
|
@ -1,5 +1,6 @@
|
||||||
# Generated by Django 5.0.8 on 2024-12-31 15:02
|
# Generated by Django 5.0.8 on 2024-12-31 15:02
|
||||||
|
|
||||||
|
import uuid
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -17,10 +18,11 @@ class Migration(migrations.Migration):
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ImmichIntegration',
|
name='ImmichIntegration',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||||
('server_url', models.CharField(max_length=255)),
|
('server_url', models.CharField(max_length=255)),
|
||||||
('api_key', models.CharField(max_length=255)),
|
('api_key', models.CharField(max_length=255)),
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 5.0.8 on 2025-01-02 17:50
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('integrations', '0002_alter_immichintegration_user'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='immichintegration',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,12 +1,15 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
import uuid
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
class ImmichIntegration(models.Model):
|
class ImmichIntegration(models.Model):
|
||||||
server_url = models.CharField(max_length=255)
|
server_url = models.CharField(max_length=255)
|
||||||
api_key = models.CharField(max_length=255)
|
api_key = models.CharField(max_length=255)
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(
|
||||||
|
User, on_delete=models.CASCADE)
|
||||||
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.user.username + ' - ' + self.server_url
|
return self.user.username + ' - ' + self.server_url
|
13
backend/server/integrations/serializers.py
Normal file
13
backend/server/integrations/serializers.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from .models import ImmichIntegration
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
class ImmichIntegrationSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ImmichIntegration
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = ['id', 'user']
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
representation = super().to_representation(instance)
|
||||||
|
representation.pop('user', None)
|
||||||
|
return representation
|
|
@ -1,11 +1,12 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from integrations.views import ImmichIntegrationView, IntegrationView
|
from integrations.views import ImmichIntegrationView, IntegrationView, ImmichIntegrationViewSet
|
||||||
|
|
||||||
# Create the router and register the ViewSet
|
# Create the router and register the ViewSet
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'immich', ImmichIntegrationView, basename='immich')
|
router.register(r'immich', ImmichIntegrationView, basename='immich')
|
||||||
router.register(r'', IntegrationView, basename='integrations')
|
router.register(r'', IntegrationView, basename='integrations')
|
||||||
|
router.register(r'immich', ImmichIntegrationViewSet, basename='immich_viewset')
|
||||||
|
|
||||||
# Include the router URLs
|
# Include the router URLs
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import os
|
import os
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import viewsets, status
|
from rest_framework import viewsets, status
|
||||||
|
|
||||||
|
from .serializers import ImmichIntegrationSerializer
|
||||||
from .models import ImmichIntegration
|
from .models import ImmichIntegration
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
@ -148,3 +150,83 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||||
},
|
},
|
||||||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class ImmichIntegrationViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
serializer_class = ImmichIntegrationSerializer
|
||||||
|
queryset = ImmichIntegration.objects.all()
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return ImmichIntegration.objects.filter(user=self.request.user)
|
||||||
|
|
||||||
|
def create(self, request):
|
||||||
|
"""
|
||||||
|
RESTful POST method for creating a new Immich integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check if the user already has an integration
|
||||||
|
user_integrations = ImmichIntegration.objects.filter(user=request.user)
|
||||||
|
if user_integrations.exists():
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'message': 'You already have an active Immich integration.',
|
||||||
|
'error': True,
|
||||||
|
'code': 'immich.integration_exists'
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = self.serializer_class(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(user=request.user)
|
||||||
|
return Response(
|
||||||
|
serializer.data,
|
||||||
|
status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors,
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
RESTful DELETE method for deleting an existing Immich integration.
|
||||||
|
"""
|
||||||
|
integration = ImmichIntegration.objects.filter(user=request.user, id=pk).first()
|
||||||
|
if not integration:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'message': 'Integration not found.',
|
||||||
|
'error': True,
|
||||||
|
'code': 'immich.integration_not_found'
|
||||||
|
},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
integration.delete()
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'message': 'Integration deleted successfully.'
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
# If the user has an integration, we only want to return that integration
|
||||||
|
|
||||||
|
user_integrations = ImmichIntegration.objects.filter(user=request.user)
|
||||||
|
if user_integrations.exists():
|
||||||
|
integration = user_integrations.first()
|
||||||
|
serializer = self.serializer_class(integration)
|
||||||
|
return Response(
|
||||||
|
serializer.data,
|
||||||
|
status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
'message': 'No integration found.',
|
||||||
|
'error': True,
|
||||||
|
'code': 'immich.integration_not_found'
|
||||||
|
},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
|
@ -191,7 +191,7 @@
|
||||||
<!-- action options dropdown -->
|
<!-- action options dropdown -->
|
||||||
|
|
||||||
{#if type != 'link'}
|
{#if type != 'link'}
|
||||||
{#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))}
|
{#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-neutral-200">
|
<div tabindex="0" role="button" class="btn btn-neutral-200">
|
||||||
<DotsHorizontal class="w-6 h-6" />
|
<DotsHorizontal class="w-6 h-6" />
|
||||||
|
|
|
@ -228,6 +228,8 @@
|
||||||
|
|
||||||
let immichSearchValue: string = '';
|
let immichSearchValue: string = '';
|
||||||
let immichError: string = '';
|
let immichError: string = '';
|
||||||
|
let immichNext: string = '';
|
||||||
|
let immichPage: number = 1;
|
||||||
|
|
||||||
async function searchImmich() {
|
async function searchImmich() {
|
||||||
let res = await fetch(`/api/integrations/immich/search/?query=${immichSearchValue}`);
|
let res = await fetch(`/api/integrations/immich/search/?query=${immichSearchValue}`);
|
||||||
|
@ -245,6 +247,44 @@
|
||||||
} else {
|
} else {
|
||||||
immichError = $t('immich.no_items_found');
|
immichError = $t('immich.no_items_found');
|
||||||
}
|
}
|
||||||
|
if (data.next) {
|
||||||
|
immichNext =
|
||||||
|
'/api/integrations/immich/search?query=' +
|
||||||
|
immichSearchValue +
|
||||||
|
'&page=' +
|
||||||
|
(immichPage + 1);
|
||||||
|
} else {
|
||||||
|
immichNext = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoreImmich() {
|
||||||
|
let res = await fetch(immichNext);
|
||||||
|
if (!res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
let errorMessage = data.message;
|
||||||
|
console.log(errorMessage);
|
||||||
|
immichError = $t(data.code);
|
||||||
|
} else {
|
||||||
|
let data = await res.json();
|
||||||
|
console.log(data);
|
||||||
|
immichError = '';
|
||||||
|
if (data.results && data.results.length > 0) {
|
||||||
|
immichImages = [...immichImages, ...data.results];
|
||||||
|
} else {
|
||||||
|
immichError = $t('immich.no_items_found');
|
||||||
|
}
|
||||||
|
if (data.next) {
|
||||||
|
immichNext =
|
||||||
|
'/api/integrations/immich/search?query=' +
|
||||||
|
immichSearchValue +
|
||||||
|
'&page=' +
|
||||||
|
(immichPage + 1);
|
||||||
|
immichPage++;
|
||||||
|
} else {
|
||||||
|
immichNext = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1077,6 +1117,11 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if immichNext}
|
||||||
|
<button class="btn btn-neutral" on:click={loadMoreImmich}
|
||||||
|
>{$t('immich.load_more')}</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -196,3 +196,9 @@ export type Category = {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
num_adventures?: number | null;
|
num_adventures?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ImmichIntegration = {
|
||||||
|
id: string;
|
||||||
|
server_url: string;
|
||||||
|
api_key: string;
|
||||||
|
};
|
||||||
|
|
|
@ -509,6 +509,7 @@
|
||||||
"query_required": "Query is required",
|
"query_required": "Query is required",
|
||||||
"server_down": "The Immich server is currently down or unreachable",
|
"server_down": "The Immich server is currently down or unreachable",
|
||||||
"no_items_found": "No items found",
|
"no_items_found": "No items found",
|
||||||
"imageid_required": "Image ID is required"
|
"imageid_required": "Image ID is required",
|
||||||
|
"load_more": "Load More"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from '../$types';
|
import type { PageServerLoad } from '../$types';
|
||||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
import type { User } from '$lib/types';
|
import type { ImmichIntegration, User } from '$lib/types';
|
||||||
import { fetchCSRFToken } from '$lib/index.server';
|
import { fetchCSRFToken } from '$lib/index.server';
|
||||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
@ -56,11 +56,22 @@ export const load: PageServerLoad = async (event) => {
|
||||||
let mfaAuthenticatorResponse = (await mfaAuthenticatorFetch.json()) as MFAAuthenticatorResponse;
|
let mfaAuthenticatorResponse = (await mfaAuthenticatorFetch.json()) as MFAAuthenticatorResponse;
|
||||||
let authenticators = (mfaAuthenticatorResponse.data.length > 0) as boolean;
|
let authenticators = (mfaAuthenticatorResponse.data.length > 0) as boolean;
|
||||||
|
|
||||||
|
let immichIntegration: ImmichIntegration | null = null;
|
||||||
|
let immichIntegrationsFetch = await fetch(`${endpoint}/api/integrations/immich/`, {
|
||||||
|
headers: {
|
||||||
|
Cookie: `sessionid=${sessionId}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (immichIntegrationsFetch.ok) {
|
||||||
|
immichIntegration = await immichIntegrationsFetch.json();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
emails,
|
emails,
|
||||||
authenticators
|
authenticators,
|
||||||
|
immichIntegration
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,14 +2,16 @@
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
import type { User } from '$lib/types.js';
|
import type { ImmichIntegration, User } from '$lib/types.js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import TotpModal from '$lib/components/TOTPModal.svelte';
|
import TotpModal from '$lib/components/TOTPModal.svelte';
|
||||||
import { appTitle, appVersion } from '$lib/config.js';
|
import { appTitle, appVersion } from '$lib/config.js';
|
||||||
|
import ImmichLogo from '$lib/assets/immich.svg';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
console.log(data);
|
||||||
let user: User;
|
let user: User;
|
||||||
let emails: typeof data.props.emails;
|
let emails: typeof data.props.emails;
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
|
@ -19,6 +21,14 @@
|
||||||
|
|
||||||
let new_email: string = '';
|
let new_email: string = '';
|
||||||
|
|
||||||
|
let immichIntegration = data.props.immichIntegration;
|
||||||
|
|
||||||
|
let newImmichIntegration: ImmichIntegration = {
|
||||||
|
server_url: '',
|
||||||
|
api_key: '',
|
||||||
|
id: ''
|
||||||
|
};
|
||||||
|
|
||||||
let isMFAModalOpen: boolean = false;
|
let isMFAModalOpen: boolean = false;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -131,6 +141,54 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function enableImmichIntegration() {
|
||||||
|
if (!immichIntegration?.id) {
|
||||||
|
let res = await fetch('/api/integrations/immich/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newImmichIntegration)
|
||||||
|
});
|
||||||
|
let data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
addToast('success', $t('settings.immich_enabled'));
|
||||||
|
immichIntegration = data;
|
||||||
|
} else {
|
||||||
|
addToast('error', $t('settings.immich_error'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let res = await fetch(`/api/integrations/immich/${immichIntegration.id}/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newImmichIntegration)
|
||||||
|
});
|
||||||
|
let data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
addToast('success', $t('settings.immich_updated'));
|
||||||
|
immichIntegration = data;
|
||||||
|
} else {
|
||||||
|
addToast('error', $t('settings.immich_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('settings.immich_disabled'));
|
||||||
|
immichIntegration = null;
|
||||||
|
} else {
|
||||||
|
addToast('error', $t('settings.immich_error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function disableMfa() {
|
async function disableMfa() {
|
||||||
const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', {
|
const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
|
@ -354,6 +412,72 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Immich Integration Section -->
|
||||||
|
<section class="space-y-8">
|
||||||
|
<h2 class="text-2xl font-semibold text-center mt-8">
|
||||||
|
Immich Integration <img
|
||||||
|
src={ImmichLogo}
|
||||||
|
alt="Immich Logo"
|
||||||
|
class="inline-block w-8 h-8 -mt-1"
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<div class="bg-neutral p-6 rounded-lg shadow-md">
|
||||||
|
<p class="text-center">
|
||||||
|
Integrate your Immich account with AdventureLog to allow you to search your photos library
|
||||||
|
and import photos for your adventures.
|
||||||
|
</p>
|
||||||
|
{#if immichIntegration}
|
||||||
|
<div class="flex flex-col items-center justify-center mt-1 space-y-2">
|
||||||
|
<div class="badge badge-success">Integration Enabled</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-warning"
|
||||||
|
on:click={() => {
|
||||||
|
if (immichIntegration) newImmichIntegration = immichIntegration;
|
||||||
|
}}>Edit</button
|
||||||
|
>
|
||||||
|
<button class="btn btn-error" on:click={disableImmichIntegration}>Disable</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !immichIntegration || newImmichIntegration.id}
|
||||||
|
<div class="mt-4">
|
||||||
|
<div>
|
||||||
|
<label for="immich_url" class="text-sm font-medium">Immich Server URL</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="immich_url"
|
||||||
|
name="immich_url"
|
||||||
|
bind:value={newImmichIntegration.server_url}
|
||||||
|
placeholder="Immich Server URL (e.g. https://immich.example.com/api)"
|
||||||
|
class="block w-full mt-1 input input-bordered input-primary"
|
||||||
|
/>
|
||||||
|
{#if newImmichIntegration.server_url && !newImmichIntegration.server_url.endsWith('api')}
|
||||||
|
<p class="text-xs text-warning mt-1">
|
||||||
|
Note: this must be the URL to the Immich API server so it likely ends with /api
|
||||||
|
unless you have a custom config.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="immich_api_key" class="text-sm font-medium">Immich API Key</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="immich_api_key"
|
||||||
|
name="immich_api_key"
|
||||||
|
bind:value={newImmichIntegration.api_key}
|
||||||
|
placeholder="Immich API Key"
|
||||||
|
class="block w-full mt-1 input input-bordered input-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button on:click={enableImmichIntegration} class="w-full mt-4 btn btn-primary py-2"
|
||||||
|
>{!immichIntegration?.id ? 'Enable Immich' : 'Edit Integration'}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Visited Region Check Section -->
|
<!-- Visited Region Check Section -->
|
||||||
<section class="text-center mt-8">
|
<section class="text-center mt-8">
|
||||||
<h2 class="text-2xl font-semibold">{$t('adventures.visited_region_check')}</h2>
|
<h2 class="text-2xl font-semibold">{$t('adventures.visited_region_check')}</h2>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue