From dc897435693ad55fac0218e34777e24b6747045f Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Tue, 31 Dec 2024 10:38:15 -0500 Subject: [PATCH 01/24] feat: add Immich integration module with API endpoints and admin interface --- backend/server/adventures/admin.py | 2 - backend/server/integrations/__init__.py | 0 backend/server/integrations/admin.py | 9 +++ backend/server/integrations/apps.py | 6 ++ .../integrations/migrations/0001_initial.py | 26 +++++++ .../integrations/migrations/__init__.py | 0 backend/server/integrations/models.py | 12 +++ backend/server/integrations/tests.py | 3 + backend/server/integrations/urls.py | 12 +++ backend/server/integrations/views.py | 77 +++++++++++++++++++ backend/server/main/settings.py | 1 + backend/server/main/urls.py | 2 + 12 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 backend/server/integrations/__init__.py create mode 100644 backend/server/integrations/admin.py create mode 100644 backend/server/integrations/apps.py create mode 100644 backend/server/integrations/migrations/0001_initial.py create mode 100644 backend/server/integrations/migrations/__init__.py create mode 100644 backend/server/integrations/models.py create mode 100644 backend/server/integrations/tests.py create mode 100644 backend/server/integrations/urls.py create mode 100644 backend/server/integrations/views.py diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index 1beac0fb..be1793b1 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -8,8 +8,6 @@ admin.autodiscover() admin.site.login = secure_admin_login(admin.site.login) - - class AdventureAdmin(admin.ModelAdmin): list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public') list_filter = ( 'user_id', 'is_public') diff --git a/backend/server/integrations/__init__.py b/backend/server/integrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/server/integrations/admin.py b/backend/server/integrations/admin.py new file mode 100644 index 00000000..d561cf40 --- /dev/null +++ b/backend/server/integrations/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from allauth.account.decorators import secure_admin_login + +from .models import ImmichIntegration + +admin.autodiscover() +admin.site.login = secure_admin_login(admin.site.login) + +admin.site.register(ImmichIntegration) \ No newline at end of file diff --git a/backend/server/integrations/apps.py b/backend/server/integrations/apps.py new file mode 100644 index 00000000..73adb7a5 --- /dev/null +++ b/backend/server/integrations/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class IntegrationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'integrations' diff --git a/backend/server/integrations/migrations/0001_initial.py b/backend/server/integrations/migrations/0001_initial.py new file mode 100644 index 00000000..73015d16 --- /dev/null +++ b/backend/server/integrations/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.8 on 2024-12-31 15:02 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ImmichIntegration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('server_url', 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)), + ], + ), + ] diff --git a/backend/server/integrations/migrations/__init__.py b/backend/server/integrations/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/server/integrations/models.py b/backend/server/integrations/models.py new file mode 100644 index 00000000..d3394cc3 --- /dev/null +++ b/backend/server/integrations/models.py @@ -0,0 +1,12 @@ +from django.db import models +from django.contrib.auth import get_user_model + +User = get_user_model() + +class ImmichIntegration(models.Model): + server_url = models.CharField(max_length=255) + api_key = models.CharField(max_length=255) + user = models.ForeignKey(User, on_delete=models.CASCADE) + + def __str__(self): + return self.user.username + ' - ' + self.server_url \ No newline at end of file diff --git a/backend/server/integrations/tests.py b/backend/server/integrations/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/backend/server/integrations/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/server/integrations/urls.py b/backend/server/integrations/urls.py new file mode 100644 index 00000000..cd1cbfa4 --- /dev/null +++ b/backend/server/integrations/urls.py @@ -0,0 +1,12 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from integrations.views import ImmichIntegrationView + +# Create the router and register the ViewSet +router = DefaultRouter() +router.register(r'immich', ImmichIntegrationView, basename='immich') + +# Include the router URLs +urlpatterns = [ + path("", include(router.urls)), # Includes /immich/ routes +] diff --git a/backend/server/integrations/views.py b/backend/server/integrations/views.py new file mode 100644 index 00000000..2622df92 --- /dev/null +++ b/backend/server/integrations/views.py @@ -0,0 +1,77 @@ +from rest_framework.response import Response +from rest_framework import viewsets, status +from .models import ImmichIntegration +from rest_framework.decorators import action +import requests + +class ImmichIntegrationView(viewsets.ViewSet): + def check_integration(self, request): + """ + Checks if the user has an active Immich integration. + Returns: + - None if the integration exists. + - A Response with an error message if the integration is missing. + """ + user_integrations = ImmichIntegration.objects.filter(user=request.user) + if not user_integrations.exists(): + return Response( + { + 'message': 'You need to have an active Immich integration to use this feature.', + 'error': True, + 'code': 'immich.integration_missing' + }, + status=status.HTTP_403_FORBIDDEN + ) + return ImmichIntegration.objects.first() + + @action(detail=False, methods=['get'], url_path='search') + def search(self, request): + """ + Handles the logic for searching Immich images. + """ + # Check for integration before proceeding + integration = self.check_integration(request) + if isinstance(integration, Response): + return integration + + query = request.query_params.get('query', '') + + if not query: + return Response( + { + 'message': 'Query is required.', + 'error': True, + 'code': 'immich.query_required' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + + immich_fetch = requests.post(f'{integration.server_url}/search/smart', headers={ + 'x-api-key': integration.api_key + }, + json = { + 'query': query + } + ) + res = immich_fetch.json() + + if 'assets' in res and 'items' in res['assets']: + return Response(res['assets']['items'], status=status.HTTP_200_OK) + else: + return Response( + { + 'message': 'No items found.', + 'error': True, + 'code': 'immich.no_items_found' + }, + status=status.HTTP_404_NOT_FOUND + ) + + + + def get(self, request): + """ + RESTful GET method for searching Immich images. + """ + return self.search(request) diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 7e4973b3..3882c471 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -56,6 +56,7 @@ 'adventures', 'worldtravel', 'users', + 'integrations', 'django.contrib.gis', ) diff --git a/backend/server/main/urls.py b/backend/server/main/urls.py index 3e3c53f8..ab1e0844 100644 --- a/backend/server/main/urls.py +++ b/backend/server/main/urls.py @@ -39,6 +39,8 @@ # path('auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent'), path("accounts/", include("allauth.urls")), + path("api/integrations/", include("integrations.urls")), + # Include the API endpoints: ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) From 15fd15ba40cf20adde92a6c2f1f98a7ed369bc56 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 1 Jan 2025 11:00:11 -0500 Subject: [PATCH 02/24] feat: update Immich integration to use OneToOneField for user and enhance image retrieval functionality --- .../0002_alter_immichintegration_user.py | 21 +++ backend/server/integrations/models.py | 2 +- backend/server/integrations/views.py | 87 +++++++++--- .../src/lib/components/AdventureModal.svelte | 125 ++++++++++-------- 4 files changed, 167 insertions(+), 68 deletions(-) create mode 100644 backend/server/integrations/migrations/0002_alter_immichintegration_user.py diff --git a/backend/server/integrations/migrations/0002_alter_immichintegration_user.py b/backend/server/integrations/migrations/0002_alter_immichintegration_user.py new file mode 100644 index 00000000..6f6912b6 --- /dev/null +++ b/backend/server/integrations/migrations/0002_alter_immichintegration_user.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.8 on 2024-12-31 18:29 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrations', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='immichintegration', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/server/integrations/models.py b/backend/server/integrations/models.py index d3394cc3..0d7a0a4a 100644 --- a/backend/server/integrations/models.py +++ b/backend/server/integrations/models.py @@ -6,7 +6,7 @@ class ImmichIntegration(models.Model): server_url = models.CharField(max_length=255) api_key = models.CharField(max_length=255) - user = models.ForeignKey(User, on_delete=models.CASCADE) + user = models.OneToOneField(User, on_delete=models.CASCADE) def __str__(self): return self.user.username + ' - ' + self.server_url \ No newline at end of file diff --git a/backend/server/integrations/views.py b/backend/server/integrations/views.py index 2622df92..72c9c4ab 100644 --- a/backend/server/integrations/views.py +++ b/backend/server/integrations/views.py @@ -1,10 +1,20 @@ +import os from rest_framework.response import Response from rest_framework import viewsets, status from .models import ImmichIntegration from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated import requests +from rest_framework.pagination import PageNumberPagination + +class StandardResultsSetPagination(PageNumberPagination): + page_size = 25 + page_size_query_param = 'page_size' + max_page_size = 1000 class ImmichIntegrationView(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + pagination_class = StandardResultsSetPagination def check_integration(self, request): """ Checks if the user has an active Immich integration. @@ -46,18 +56,35 @@ def search(self, request): status=status.HTTP_400_BAD_REQUEST ) + # check so if the server is down, it does not tweak out like a madman and crash the server with a 500 error code + try: + immich_fetch = requests.post(f'{integration.server_url}/search/smart', headers={ + 'x-api-key': integration.api_key + }, + json = { + 'query': query + } + ) + res = immich_fetch.json() + except requests.exceptions.ConnectionError: + return Response( + { + 'message': 'The Immich server is currently down or unreachable.', + 'error': True, + 'code': 'immich.server_down' + }, + status=status.HTTP_503_SERVICE_UNAVAILABLE + ) - immich_fetch = requests.post(f'{integration.server_url}/search/smart', headers={ - 'x-api-key': integration.api_key - }, - json = { - 'query': query - } - ) - res = immich_fetch.json() - if 'assets' in res and 'items' in res['assets']: - return Response(res['assets']['items'], status=status.HTTP_200_OK) + paginator = self.pagination_class() + # for each item in the items, we need to add the image url to the item so we can display it in the frontend + public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/') + public_url = public_url.replace("'", "") + for item in res['assets']['items']: + item['image_url'] = f'{public_url}/api/integrations/immich/get/{item["id"]}' + result_page = paginator.paginate_queryset(res['assets']['items'], request) + return paginator.get_paginated_response(result_page) else: return Response( { @@ -68,10 +95,40 @@ def search(self, request): status=status.HTTP_404_NOT_FOUND ) - - - def get(self, request): + @action(detail=False, methods=['get'], url_path='get/(?P[^/.]+)') + def get(self, request, imageid=None): """ - RESTful GET method for searching Immich images. + RESTful GET method for retrieving a specific Immich image by ID. """ - return self.search(request) + # Check for integration before proceeding + integration = self.check_integration(request) + if isinstance(integration, Response): + return integration + + if not imageid: + return Response( + { + 'message': 'Image ID is required.', + 'error': True, + 'code': 'immich.imageid_required' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # check so if the server is down, it does not tweak out like a madman and crash the server with a 500 error code + try: + immich_fetch = requests.get(f'{integration.server_url}/assets/{imageid}/thumbnail?size=preview', headers={ + 'x-api-key': integration.api_key + }) + # should return the image file + from django.http import HttpResponse + return HttpResponse(immich_fetch.content, content_type='image/jpeg', status=status.HTTP_200_OK) + except requests.exceptions.ConnectionError: + return Response( + { + 'message': 'The Immich server is currently down or unreachable.', + 'error': True, + 'code': 'immich.server_down' + }, + status=status.HTTP_503_SERVICE_UNAVAILABLE + ) diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index ff9770d5..cc081a0b 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -915,84 +915,105 @@ it would also work to just use on:click on the MapLibre component itself. --> {:else} -

{$t('adventures.upload_images_here')}

- -
-
-
-
- - - -
-
-
-
+

{$t('adventures.upload_images_here')}

+ +
+ +
+ + + +
+
+ +
+ +
- +
-
-
+
+ +
+ +
- +
-
- {#if images.length > 0} -

{$t('adventures.my_images')}

- {:else} -

{$t('adventures.no_images')}

- {/if} -
+
+ +
+ + {#if images.length > 0} +

{$t('adventures.my_images')}

+
{#each images as image}
- {image.id} + {image.id}
{/each}
-
-
- + {:else} +

{$t('adventures.no_images')}

+ {/if} + +
+
{/if} + {#if adventure.is_public && adventure.id}

{$t('adventures.share_adventure')}

From 67f6af8ca36de1006e78f4efdc0ea4178c45f9dc Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 1 Jan 2025 16:24:44 -0500 Subject: [PATCH 03/24] feat: add Immich integration view and API documentation, enhance error handling, and include SVG asset --- backend/server/integrations/urls.py | 3 +- backend/server/integrations/views.py | 16 ++ backend/server/main/settings.py | 8 - backend/server/templates/base.html | 2 + frontend/src/lib/assets/immich.svg | 1 + .../src/lib/components/AdventureModal.svelte | 149 +++++++++++++++--- frontend/src/locales/en.json | 9 ++ frontend/src/routes/immich/[key]/+server.ts | 54 +++++++ 8 files changed, 208 insertions(+), 34 deletions(-) create mode 100644 frontend/src/lib/assets/immich.svg create mode 100644 frontend/src/routes/immich/[key]/+server.ts diff --git a/backend/server/integrations/urls.py b/backend/server/integrations/urls.py index cd1cbfa4..df405f18 100644 --- a/backend/server/integrations/urls.py +++ b/backend/server/integrations/urls.py @@ -1,10 +1,11 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from integrations.views import ImmichIntegrationView +from integrations.views import ImmichIntegrationView, IntegrationView # Create the router and register the ViewSet router = DefaultRouter() router.register(r'immich', ImmichIntegrationView, basename='immich') +router.register(r'', IntegrationView, basename='integrations') # Include the router URLs urlpatterns = [ diff --git a/backend/server/integrations/views.py b/backend/server/integrations/views.py index 72c9c4ab..7a05fa34 100644 --- a/backend/server/integrations/views.py +++ b/backend/server/integrations/views.py @@ -7,6 +7,22 @@ import requests from rest_framework.pagination import PageNumberPagination +class IntegrationView(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + def list(self, request): + """ + RESTful GET method for listing all integrations. + """ + immich_integrations = ImmichIntegration.objects.filter(user=request.user) + + return Response( + { + 'immich': immich_integrations.exists() + }, + status=status.HTTP_200_OK + ) + + class StandardResultsSetPagination(PageNumberPagination): page_size = 25 page_size_query_param = 'page_size' diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 3882c471..b934b990 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -165,9 +165,6 @@ DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True' DISABLE_REGISTRATION_MESSAGE = getenv('DISABLE_REGISTRATION_MESSAGE', 'Registration is disabled. Please contact the administrator if you need an account.') -ALLAUTH_UI_THEME = "dark" -SILENCED_SYSTEM_CHECKS = ["slippers.E001"] - AUTH_USER_MODEL = 'users.CustomUser' ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter' @@ -223,11 +220,6 @@ 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', } -SWAGGER_SETTINGS = { - 'LOGIN_URL': 'login', - 'LOGOUT_URL': 'logout', -} - CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()] diff --git a/backend/server/templates/base.html b/backend/server/templates/base.html index be712b76..9e1d48cf 100644 --- a/backend/server/templates/base.html +++ b/backend/server/templates/base.html @@ -53,6 +53,7 @@ >Documentation +
  • Source Code
  • +
  • API Docs
  • diff --git a/frontend/src/lib/assets/immich.svg b/frontend/src/lib/assets/immich.svg new file mode 100644 index 00000000..70aa6727 --- /dev/null +++ b/frontend/src/lib/assets/immich.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index cc081a0b..f405b5b0 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -13,7 +13,7 @@ import { addToast } from '$lib/toasts'; import { deserialize } from '$app/forms'; import { t } from 'svelte-i18n'; - + import ImmichLogo from '$lib/assets/immich.svg'; export let longitude: number | null = null; export let latitude: number | null = null; export let collection: Collection | null = null; @@ -180,28 +180,71 @@ } async function fetchImage() { - let res = await fetch(url); - let data = await res.blob(); - if (!data) { - imageError = $t('adventures.no_image_url'); - return; + try { + let res = await fetch(url); + let data = await res.blob(); + if (!data) { + imageError = $t('adventures.no_image_url'); + return; + } + let file = new File([data], 'image.jpg', { type: 'image/jpeg' }); + let formData = new FormData(); + formData.append('image', file); + formData.append('adventure', adventure.id); + + let res2 = await fetch(`/adventures?/image`, { + method: 'POST', + body: formData + }); + let data2 = await res2.json(); + + if (data2.type === 'success') { + console.log('Response Data:', data2); + + // Deserialize the nested data + let rawData = JSON.parse(data2.data); // Parse the data field + console.log('Deserialized Data:', rawData); + + // Assuming the first object in the array is the new image + let newImage = { + id: rawData[0].id, + image: rawData[2] // This is the URL for the image + }; + console.log('New Image:', newImage); + + // Update images and adventure + images = [...images, newImage]; + adventure.images = images; + + addToast('success', $t('adventures.image_upload_success')); + } else { + addToast('error', $t('adventures.image_upload_error')); + } + } catch (error) { + console.error('Error in fetchImage:', error); + addToast('error', $t('adventures.image_upload_error')); } - let file = new File([data], 'image.jpg', { type: 'image/jpeg' }); - let formData = new FormData(); - formData.append('image', file); - formData.append('adventure', adventure.id); - let res2 = await fetch(`/adventures?/image`, { - method: 'POST', - body: formData - }); - let data2 = await res2.json(); - console.log(data2); - if (data2.type === 'success') { - images = [...images, data2]; - adventure.images = images; - addToast('success', $t('adventures.image_upload_success')); + } + + let immichSearchValue: string = ''; + let immichError: string = ''; + + async function searchImmich() { + let res = await fetch(`/api/integrations/immich/search/?query=${immichSearchValue}`); + if (!res.ok) { + let data = await res.json(); + let errorMessage = data.message; + console.log(errorMessage); + immichError = $t(data.code); } else { - addToast('error', $t('adventures.image_upload_error')); + let data = await res.json(); + console.log(data); + immichError = ''; + if (data.results && data.results.length > 0) { + immichImages = data.results; + } else { + immichError = $t('immich.no_items_found'); + } } } @@ -337,6 +380,9 @@ const dispatch = createEventDispatcher(); let modal: HTMLDialogElement; + let immichIntegration: boolean = false; + let immichImages: any[] = []; + onMount(async () => { modal = document.getElementById('my_modal_1') as HTMLDialogElement; modal.showModal(); @@ -347,6 +393,16 @@ } else { addToast('error', $t('adventures.category_fetch_error')); } + // Check for Immich Integration + let res = await fetch('/api/integrations'); + if (!res.ok) { + addToast('error', $t('immich.integration_fetch_error')); + } else { + let data = await res.json(); + if (data.immich) { + immichIntegration = true; + } + } }); function close() { @@ -915,10 +971,10 @@ it would also work to just use on:click on the MapLibre component itself. -->
    {:else} -

    {$t('adventures.upload_images_here')}

    +

    {$t('adventures.upload_images_here')}

    -
    -
    + +
    +

    + Immich Integration Immich Logo +

    +
    +

    + Integrate your Immich account with AdventureLog to allow you to search your photos library + and import photos for your adventures. +

    + {#if immichIntegration} +
    +
    Integration Enabled
    +
    + + +
    +
    + {/if} + {#if !immichIntegration || newImmichIntegration.id} +
    +
    + + + {#if newImmichIntegration.server_url && !newImmichIntegration.server_url.endsWith('api')} +

    + Note: this must be the URL to the Immich API server so it likely ends with /api + unless you have a custom config. +

    + {/if} +
    +
    + + +
    + +
    + {/if} +
    +
    +

    {$t('adventures.visited_region_check')}

    From 81b60d60212d71b2fb97abfc652eaac7e38205cc Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 2 Jan 2025 13:40:02 -0500 Subject: [PATCH 06/24] feat: enhance settings page with text-neutral styling for labels and messages --- frontend/src/routes/settings/+page.svelte | 44 +++++++++++++++-------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index aa3bc08c..800084a2 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -232,7 +232,9 @@ class="space-y-6" >
    - +
    - +
    - +
    - + - +
    @@ -298,7 +308,7 @@
    -
    - +
    - {/each} {#if emails.length === 0} -

    {$t('settings.no_email_set')}

    +

    {$t('settings.no_email_set')}

    {/if}
    @@ -400,7 +412,7 @@
    {#if !data.props.authenticators} -

    {$t('settings.mfa_not_enabled')}

    +

    {$t('settings.mfa_not_enabled')}

    @@ -422,7 +434,7 @@ />
    -

    +

    Integrate your Immich account with AdventureLog to allow you to search your photos library and import photos for your adventures.

    @@ -443,7 +455,9 @@ {#if !immichIntegration || newImmichIntegration.id}
    - + {#if newImmichIntegration.server_url && !newImmichIntegration.server_url.endsWith('api')} -

    +

    Note: this must be the URL to the Immich API server so it likely ends with /api unless you have a custom config.

    {/if}
    - + Date: Thu, 2 Jan 2025 17:56:47 -0500 Subject: [PATCH 07/24] feat: add Immich album retrieval functionality and implement album selection component --- backend/server/integrations/views.py | 82 +++++++++ .../src/lib/components/AdventureModal.svelte | 120 +------------ .../src/lib/components/ImmichSelect.svelte | 159 ++++++++++++++++++ frontend/src/lib/types.ts | 28 +++ 4 files changed, 278 insertions(+), 111 deletions(-) create mode 100644 frontend/src/lib/components/ImmichSelect.svelte diff --git a/backend/server/integrations/views.py b/backend/server/integrations/views.py index 673fad5a..935e28c2 100644 --- a/backend/server/integrations/views.py +++ b/backend/server/integrations/views.py @@ -150,6 +150,88 @@ def get(self, request, imageid=None): }, status=status.HTTP_503_SERVICE_UNAVAILABLE ) + + @action(detail=False, methods=['get']) + def albums(self, request): + """ + RESTful GET method for retrieving all Immich albums. + """ + # Check for integration before proceeding + integration = self.check_integration(request) + if isinstance(integration, Response): + return integration + + # check so if the server is down, it does not tweak out like a madman and crash the server with a 500 error code + try: + immich_fetch = requests.get(f'{integration.server_url}/albums', headers={ + 'x-api-key': integration.api_key + }) + res = immich_fetch.json() + except requests.exceptions.ConnectionError: + return Response( + { + 'message': 'The Immich server is currently down or unreachable.', + 'error': True, + 'code': 'immich.server_down' + }, + status=status.HTTP_503_SERVICE_UNAVAILABLE + ) + + return Response( + res, + status=status.HTTP_200_OK + ) + + @action(detail=False, methods=['get'], url_path='albums/(?P[^/.]+)') + def album(self, request, albumid=None): + """ + RESTful GET method for retrieving a specific Immich album by ID. + """ + # Check for integration before proceeding + integration = self.check_integration(request) + if isinstance(integration, Response): + return integration + + if not albumid: + return Response( + { + 'message': 'Album ID is required.', + 'error': True, + 'code': 'immich.albumid_required' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + # check so if the server is down, it does not tweak out like a madman and crash the server with a 500 error code + try: + immich_fetch = requests.get(f'{integration.server_url}/albums/{albumid}', headers={ + 'x-api-key': integration.api_key + }) + res = immich_fetch.json() + except requests.exceptions.ConnectionError: + return Response( + { + 'message': 'The Immich server is currently down or unreachable.', + 'error': True, + 'code': 'immich.server_down' + }, + status=status.HTTP_503_SERVICE_UNAVAILABLE + ) + + if 'assets' in res: + return Response( + res['assets'], + status=status.HTTP_200_OK + ) + else: + return Response( + { + 'message': 'No assets found in this album.', + 'error': True, + 'code': 'immich.no_assets_found' + }, + status=status.HTTP_404_NOT_FOUND + ) class ImmichIntegrationViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index d5e4d890..550c4194 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -13,7 +13,6 @@ import { addToast } from '$lib/toasts'; import { deserialize } from '$app/forms'; import { t } from 'svelte-i18n'; - import ImmichLogo from '$lib/assets/immich.svg'; export let longitude: number | null = null; export let latitude: number | null = null; export let collection: Collection | null = null; @@ -33,6 +32,7 @@ import CategoryDropdown from './CategoryDropdown.svelte'; import { findFirstValue } from '$lib'; import MarkdownEditor from './MarkdownEditor.svelte'; + import ImmichSelect from './ImmichSelect.svelte'; let wikiError: string = ''; @@ -207,7 +207,7 @@ // Assuming the first object in the array is the new image let newImage = { - id: rawData[0].id, + id: rawData[1], image: rawData[2] // This is the URL for the image }; console.log('New Image:', newImage); @@ -217,6 +217,7 @@ adventure.images = images; addToast('success', $t('adventures.image_upload_success')); + url = ''; } else { addToast('error', $t('adventures.image_upload_error')); } @@ -226,68 +227,6 @@ } } - let immichSearchValue: string = ''; - let immichError: string = ''; - let immichNext: string = ''; - let immichPage: number = 1; - - async function searchImmich() { - let res = await fetch(`/api/integrations/immich/search/?query=${immichSearchValue}`); - 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 = data.results; - } else { - 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 = ''; - } - } - } - async function fetchWikiImage() { let res = await fetch(`/api/generate/img/?name=${imageSearch}`); let data = await res.json(); @@ -421,7 +360,6 @@ let modal: HTMLDialogElement; let immichIntegration: boolean = false; - let immichImages: any[] = []; onMount(async () => { modal = document.getElementById('my_modal_1') as HTMLDialogElement; @@ -1078,52 +1016,12 @@ it would also work to just use on:click on the MapLibre component itself. -->
    {#if immichIntegration} -
    - - -
    - - -
    -

    {immichError}

    -
    - {#each immichImages as image} -
    - - Image from Immich - -
    - {/each} - {#if immichNext} - - {/if} -
    -
    + { + url = e.detail; + fetchImage(); + }} + /> {/if}
    diff --git a/frontend/src/lib/components/ImmichSelect.svelte b/frontend/src/lib/components/ImmichSelect.svelte new file mode 100644 index 00000000..d5602b9d --- /dev/null +++ b/frontend/src/lib/components/ImmichSelect.svelte @@ -0,0 +1,159 @@ + + +
    + +
    + + + {#if searchOrSelect === 'search'} + + + {:else} + + {/if} +
    + +

    {immichError}

    +
    + {#each immichImages as image} +
    + + Image from Immich + +
    + {/each} + {#if immichNext} + + {/if} +
    +
    diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 75cee6e4..a2e6d3ca 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -202,3 +202,31 @@ export type ImmichIntegration = { server_url: string; api_key: string; }; + +export type ImmichAlbum = { + albumName: string; + description: string; + albumThumbnailAssetId: string; + createdAt: string; + updatedAt: string; + id: string; + ownerId: string; + owner: { + id: string; + email: string; + name: string; + profileImagePath: string; + avatarColor: string; + profileChangedAt: string; + }; + albumUsers: any[]; + shared: boolean; + hasSharedLink: boolean; + startDate: string; + endDate: string; + assets: any[]; + assetCount: number; + isActivityEnabled: boolean; + order: string; + lastModifiedAssetTimestamp: string; +}; From eea87e59a5abafb95585584d1ff79913c4e6d158 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 2 Jan 2025 18:34:13 -0500 Subject: [PATCH 08/24] feat: update Immich integration migrations and enhance localization strings for user feedback --- .../integrations/migrations/0001_initial.py | 7 +-- .../0002_alter_immichintegration_user.py | 21 ------- .../0003_alter_immichintegration_user.py | 21 ------- .../src/lib/components/ImmichSelect.svelte | 58 ++++++++++++------- frontend/src/locales/de.json | 26 ++++++++- frontend/src/locales/en.json | 15 ++++- frontend/src/locales/es.json | 26 ++++++++- frontend/src/locales/fr.json | 26 ++++++++- frontend/src/locales/it.json | 26 ++++++++- frontend/src/locales/nl.json | 26 ++++++++- frontend/src/locales/pl.json | 26 ++++++++- frontend/src/locales/sv.json | 27 ++++++++- frontend/src/locales/zh.json | 26 ++++++++- frontend/src/routes/settings/+page.svelte | 43 +++++++------- 14 files changed, 275 insertions(+), 99 deletions(-) delete mode 100644 backend/server/integrations/migrations/0002_alter_immichintegration_user.py delete mode 100644 backend/server/integrations/migrations/0003_alter_immichintegration_user.py diff --git a/backend/server/integrations/migrations/0001_initial.py b/backend/server/integrations/migrations/0001_initial.py index 0b05b2a5..1bf029b3 100644 --- a/backend/server/integrations/migrations/0001_initial.py +++ b/backend/server/integrations/migrations/0001_initial.py @@ -1,7 +1,7 @@ -# Generated by Django 5.0.8 on 2024-12-31 15:02 +# Generated by Django 5.0.8 on 2025-01-02 23:16 -import uuid import django.db.models.deletion +import uuid from django.conf import settings from django.db import migrations, models @@ -18,11 +18,10 @@ class Migration(migrations.Migration): migrations.CreateModel( name='ImmichIntegration', fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('server_url', models.CharField(max_length=255)), ('api_key', models.CharField(max_length=255)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), ] - diff --git a/backend/server/integrations/migrations/0002_alter_immichintegration_user.py b/backend/server/integrations/migrations/0002_alter_immichintegration_user.py deleted file mode 100644 index 6f6912b6..00000000 --- a/backend/server/integrations/migrations/0002_alter_immichintegration_user.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.0.8 on 2024-12-31 18:29 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('integrations', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterField( - model_name='immichintegration', - name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/backend/server/integrations/migrations/0003_alter_immichintegration_user.py b/backend/server/integrations/migrations/0003_alter_immichintegration_user.py deleted file mode 100644 index 30bd443f..00000000 --- a/backend/server/integrations/migrations/0003_alter_immichintegration_user.py +++ /dev/null @@ -1,21 +0,0 @@ -# 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), - ), - ] diff --git a/frontend/src/lib/components/ImmichSelect.svelte b/frontend/src/lib/components/ImmichSelect.svelte index d5602b9d..8668f6ff 100644 --- a/frontend/src/lib/components/ImmichSelect.svelte +++ b/frontend/src/lib/components/ImmichSelect.svelte @@ -13,6 +13,7 @@ $: { if (currentAlbum) { + immichImages = []; fetchAlbumAssets(currentAlbum); } else { immichImages = []; @@ -104,29 +105,44 @@ {$t('immich.immich')} Immich Logo -
    - - - {#if searchOrSelect === 'search'} +
    +
    (currentAlbum = '')} + type="radio" + class="join-item btn" + bind:group={searchOrSelect} + value="search" + aria-label="Search" /> - - {:else} - - {/if} + +
    +
    + {#if searchOrSelect === 'search'} + + + + + {:else} + + {/if} +

    {immichError}

    diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index c2485cac..09043920 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -215,7 +215,8 @@ "start": "Start", "starting_airport": "Startflughafen", "to": "Zu", - "transportation_delete_confirm": "Sind Sie sicher, dass Sie diesen Transport löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden." + "transportation_delete_confirm": "Sind Sie sicher, dass Sie diesen Transport löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.", + "show_map": "Karte anzeigen" }, "home": { "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", @@ -500,5 +501,28 @@ "total_adventures": "Totale Abenteuer", "total_visited_regions": "Insgesamt besuchte Regionen", "welcome_back": "Willkommen zurück" + }, + "immich": { + "api_key": "Immich-API-Schlüssel", + "api_note": "Hinweis: Dies muss die URL zum Immich-API-Server sein, daher endet sie wahrscheinlich mit /api, es sei denn, Sie haben eine benutzerdefinierte Konfiguration.", + "disable": "Deaktivieren", + "enable_immich": "Immich aktivieren", + "imageid_required": "Bild-ID ist erforderlich", + "immich": "Immich", + "immich_desc": "Integrieren Sie Ihr Immich-Konto mit AdventureLog, damit Sie Ihre Fotobibliothek durchsuchen und Fotos für Ihre Abenteuer importieren können.", + "immich_disabled": "Immich-Integration erfolgreich deaktiviert!", + "immich_enabled": "Immich-Integration erfolgreich aktiviert!", + "immich_error": "Fehler beim Aktualisieren der Immich-Integration", + "immich_updated": "Immich-Einstellungen erfolgreich aktualisiert!", + "integration_enabled": "Integration aktiviert", + "integration_fetch_error": "Fehler beim Abrufen der Daten aus der Immich-Integration", + "integration_missing": "Im Backend fehlt die Immich-Integration", + "load_more": "Mehr laden", + "no_items_found": "Keine Artikel gefunden", + "query_required": "Abfrage ist erforderlich", + "server_down": "Der Immich-Server ist derzeit ausgefallen oder nicht erreichbar", + "server_url": "Immich-Server-URL", + "update_integration": "Update-Integration", + "immich_integration": "Immich-Integration" } } diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index a065e79c..2acebc9e 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -510,6 +510,19 @@ "server_down": "The Immich server is currently down or unreachable", "no_items_found": "No items found", "imageid_required": "Image ID is required", - "load_more": "Load More" + "load_more": "Load More", + "immich_updated": "Immich settings updated successfully!", + "immich_enabled": "Immich integration enabled successfully!", + "immich_error": "Error updating Immich integration", + "immich_disabled": "Immich integration disabled successfully!", + "immich_desc": "Integrate your Immich account with AdventureLog to allow you to search your photos library and import photos for your adventures.", + "integration_enabled": "Integration Enabled", + "disable": "Disable", + "server_url": "Immich Server URL", + "api_note": "Note: this must be the URL to the Immich API server so it likely ends with /api unless you have a custom config.", + "api_key": "Immich API Key", + "enable_immich": "Enable Immich", + "update_integration": "Update Integration", + "immich_integration": "Immich Integration" } } diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 1164d25e..aa23a71b 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -262,7 +262,8 @@ "start": "Comenzar", "starting_airport": "Aeropuerto de inicio", "to": "A", - "transportation_delete_confirm": "¿Está seguro de que desea eliminar este transporte? \nEsta acción no se puede deshacer." + "transportation_delete_confirm": "¿Está seguro de que desea eliminar este transporte? \nEsta acción no se puede deshacer.", + "show_map": "Mostrar mapa" }, "worldtravel": { "all": "Todo", @@ -500,5 +501,28 @@ "total_adventures": "Aventuras totales", "total_visited_regions": "Total de regiones visitadas", "welcome_back": "Bienvenido de nuevo" + }, + "immich": { + "api_key": "Clave API de Immich", + "api_note": "Nota: esta debe ser la URL del servidor API de Immich, por lo que probablemente termine con /api a menos que tenga una configuración personalizada.", + "disable": "Desactivar", + "enable_immich": "Habilitar Immich", + "imageid_required": "Se requiere identificación con imagen", + "immich": "immicha", + "immich_desc": "Integre su cuenta de Immich con AdventureLog para permitirle buscar en su biblioteca de fotos e importar fotos para sus aventuras.", + "immich_disabled": "¡La integración de Immich se deshabilitó exitosamente!", + "immich_enabled": "¡La integración de Immich se habilitó exitosamente!", + "immich_error": "Error al actualizar la integración de Immich", + "immich_updated": "¡La configuración de Immich se actualizó exitosamente!", + "integration_enabled": "Integración habilitada", + "integration_fetch_error": "Error al obtener datos de la integración de Immich", + "integration_missing": "Falta la integración de Immich en el backend", + "load_more": "Cargar más", + "no_items_found": "No se encontraron artículos", + "query_required": "Se requiere consulta", + "server_down": "El servidor Immich está actualmente inactivo o inaccesible", + "server_url": "URL del servidor Immich", + "update_integration": "Integración de actualización", + "immich_integration": "Integración Immich" } } diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index c66bf036..eaa8842e 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -215,7 +215,8 @@ "start": "Commencer", "starting_airport": "Aéroport de départ", "to": "À", - "transportation_delete_confirm": "Etes-vous sûr de vouloir supprimer ce transport ? \nCette action ne peut pas être annulée." + "transportation_delete_confirm": "Etes-vous sûr de vouloir supprimer ce transport ? \nCette action ne peut pas être annulée.", + "show_map": "Afficher la carte" }, "home": { "desc_1": "Découvrez, planifiez et explorez en toute simplicité", @@ -500,5 +501,28 @@ "total_adventures": "Aventures totales", "total_visited_regions": "Total des régions visitées", "welcome_back": "Content de te revoir" + }, + "immich": { + "api_key": "Clé API Immich", + "api_note": "Remarque : il doit s'agir de l'URL du serveur API Immich, elle se termine donc probablement par /api, sauf si vous disposez d'une configuration personnalisée.", + "disable": "Désactiver", + "enable_immich": "Activer Immich", + "imageid_required": "L'identifiant de l'image est requis", + "immich": "Immich", + "immich_desc": "Intégrez votre compte Immich à AdventureLog pour vous permettre de rechercher dans votre bibliothèque de photos et d'importer des photos pour vos aventures.", + "immich_disabled": "Intégration Immich désactivée avec succès !", + "immich_enabled": "Intégration Immich activée avec succès !", + "immich_error": "Erreur lors de la mise à jour de l'intégration Immich", + "immich_integration": "Intégration Immich", + "immich_updated": "Paramètres Immich mis à jour avec succès !", + "integration_enabled": "Intégration activée", + "integration_fetch_error": "Erreur lors de la récupération des données de l'intégration Immich", + "integration_missing": "L'intégration Immich est absente du backend", + "load_more": "Charger plus", + "no_items_found": "Aucun article trouvé", + "query_required": "La requête est obligatoire", + "server_down": "Le serveur Immich est actuellement en panne ou inaccessible", + "server_url": "URL du serveur Immich", + "update_integration": "Intégration des mises à jour" } } diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 21bee779..0b880a93 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -215,7 +215,8 @@ "start": "Inizio", "starting_airport": "Inizio aeroporto", "to": "A", - "transportation_delete_confirm": "Sei sicuro di voler eliminare questo trasporto? \nQuesta azione non può essere annullata." + "transportation_delete_confirm": "Sei sicuro di voler eliminare questo trasporto? \nQuesta azione non può essere annullata.", + "show_map": "Mostra mappa" }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", @@ -500,5 +501,28 @@ "total_adventures": "Avventure totali", "total_visited_regions": "Totale regioni visitate", "welcome_back": "Bentornato" + }, + "immich": { + "api_key": "Chiave API Immich", + "api_note": "Nota: questo deve essere l'URL del server API Immich, quindi probabilmente termina con /api a meno che tu non abbia una configurazione personalizzata.", + "disable": "Disabilita", + "enable_immich": "Abilita Immich", + "imageid_required": "L'ID immagine è obbligatorio", + "immich": "Immich", + "immich_desc": "Integra il tuo account Immich con AdventureLog per consentirti di cercare nella tua libreria di foto e importare foto per le tue avventure.", + "immich_disabled": "Integrazione Immich disabilitata con successo!", + "immich_enabled": "Integrazione Immich abilitata con successo!", + "immich_error": "Errore durante l'aggiornamento dell'integrazione Immich", + "immich_integration": "Integrazione di Immich", + "immich_updated": "Impostazioni Immich aggiornate con successo!", + "integration_enabled": "Integrazione abilitata", + "integration_fetch_error": "Errore durante il recupero dei dati dall'integrazione Immich", + "integration_missing": "L'integrazione Immich manca dal backend", + "load_more": "Carica altro", + "no_items_found": "Nessun articolo trovato", + "query_required": "La domanda è obbligatoria", + "server_down": "Il server Immich è attualmente inattivo o irraggiungibile", + "server_url": "URL del server Immich", + "update_integration": "Aggiorna integrazione" } } diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index c2a59a57..f00fac4d 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -215,7 +215,8 @@ "starting_airport": "Startende luchthaven", "to": "Naar", "transportation_delete_confirm": "Weet u zeker dat u dit transport wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.", - "ending_airport": "Einde luchthaven" + "ending_airport": "Einde luchthaven", + "show_map": "Toon kaart" }, "home": { "desc_1": "Ontdek, plan en verken met gemak", @@ -500,5 +501,28 @@ "total_adventures": "Totale avonturen", "total_visited_regions": "Totaal bezochte regio's", "welcome_back": "Welkom terug" + }, + "immich": { + "api_key": "Immich API-sleutel", + "api_note": "Let op: dit moet de URL naar de Immich API-server zijn, dus deze eindigt waarschijnlijk op /api, tenzij je een aangepaste configuratie hebt.", + "disable": "Uitzetten", + "enable_immich": "Schakel Immich in", + "imageid_required": "Afbeeldings-ID is vereist", + "immich": "Immich", + "immich_desc": "Integreer uw Immich-account met AdventureLog zodat u in uw fotobibliotheek kunt zoeken en foto's voor uw avonturen kunt importeren.", + "immich_disabled": "Immich-integratie succesvol uitgeschakeld!", + "immich_enabled": "Immich-integratie succesvol ingeschakeld!", + "immich_error": "Fout bij updaten van Immich-integratie", + "immich_integration": "Immich-integratie", + "immich_updated": "Immich-instellingen zijn succesvol bijgewerkt!", + "integration_enabled": "Integratie ingeschakeld", + "integration_fetch_error": "Fout bij het ophalen van gegevens uit de Immich-integratie", + "integration_missing": "De Immich-integratie ontbreekt in de backend", + "load_more": "Laad meer", + "no_items_found": "Geen artikelen gevonden", + "query_required": "Er is een zoekopdracht vereist", + "server_down": "De Immich-server is momenteel offline of onbereikbaar", + "server_url": "Immich-server-URL", + "update_integration": "Integratie bijwerken" } } diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index eac96345..b9a13cfb 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -262,7 +262,8 @@ "start": "Start", "starting_airport": "Początkowe lotnisko", "to": "Do", - "transportation_delete_confirm": "Czy na pewno chcesz usunąć ten transport? \nTej akcji nie można cofnąć." + "transportation_delete_confirm": "Czy na pewno chcesz usunąć ten transport? \nTej akcji nie można cofnąć.", + "show_map": "Pokaż mapę" }, "worldtravel": { "country_list": "Lista krajów", @@ -500,5 +501,28 @@ "total_adventures": "Totalne przygody", "total_visited_regions": "Łączna liczba odwiedzonych regionów", "welcome_back": "Witamy z powrotem" + }, + "immich": { + "api_key": "Klucz API Immicha", + "api_note": "Uwaga: musi to być adres URL serwera API Immich, więc prawdopodobnie kończy się na /api, chyba że masz niestandardową konfigurację.", + "disable": "Wyłączyć", + "enable_immich": "Włącz Immicha", + "immich": "Immich", + "immich_enabled": "Integracja z Immich została pomyślnie włączona!", + "immich_error": "Błąd podczas aktualizacji integracji Immich", + "immich_integration": "Integracja Immicha", + "immich_updated": "Ustawienia Immich zostały pomyślnie zaktualizowane!", + "integration_enabled": "Integracja włączona", + "integration_fetch_error": "Błąd podczas pobierania danych z integracji Immich", + "integration_missing": "W backendie brakuje integracji z Immich", + "load_more": "Załaduj więcej", + "no_items_found": "Nie znaleziono żadnych elementów", + "query_required": "Zapytanie jest wymagane", + "server_down": "Serwer Immich jest obecnie wyłączony lub nieosiągalny", + "server_url": "Adres URL serwera Immich", + "update_integration": "Zaktualizuj integrację", + "imageid_required": "Wymagany jest identyfikator obrazu", + "immich_desc": "Zintegruj swoje konto Immich z AdventureLog, aby móc przeszukiwać bibliotekę zdjęć i importować zdjęcia do swoich przygód.", + "immich_disabled": "Integracja z Immich została pomyślnie wyłączona!" } } diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 7beccea6..a4c443ca 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -1,5 +1,4 @@ { - "about": { "about": "Om", "close": "Stäng", @@ -216,7 +215,8 @@ "start": "Start", "starting_airport": "Startar flygplats", "to": "Till", - "transportation_delete_confirm": "Är du säker på att du vill ta bort denna transport? \nDenna åtgärd kan inte ångras." + "transportation_delete_confirm": "Är du säker på att du vill ta bort denna transport? \nDenna åtgärd kan inte ångras.", + "show_map": "Visa karta" }, "home": { "desc_1": "Upptäck, planera och utforska med lätthet", @@ -501,5 +501,28 @@ "total_adventures": "Totala äventyr", "total_visited_regions": "Totalt antal besökta regioner", "welcome_back": "Välkommen tillbaka" + }, + "immich": { + "api_key": "Immich API-nyckel", + "api_note": "Obs: detta måste vara URL:en till Immich API-servern så den slutar troligen med /api om du inte har en anpassad konfiguration.", + "disable": "Inaktivera", + "enable_immich": "Aktivera Immich", + "imageid_required": "Bild-ID krävs", + "immich": "Immich", + "immich_desc": "Integrera ditt Immich-konto med AdventureLog så att du kan söka i ditt fotobibliotek och importera bilder för dina äventyr.", + "immich_disabled": "Immich-integrationen inaktiverades framgångsrikt!", + "immich_enabled": "Immich-integrationen har aktiverats framgångsrikt!", + "immich_error": "Fel vid uppdatering av Immich-integration", + "immich_integration": "Immich Integration", + "immich_updated": "Immich-inställningarna har uppdaterats framgångsrikt!", + "integration_enabled": "Integration aktiverad", + "integration_fetch_error": "Fel vid hämtning av data från Immich-integrationen", + "integration_missing": "Immich-integrationen saknas i backend", + "load_more": "Ladda mer", + "no_items_found": "Inga föremål hittades", + "query_required": "Fråga krävs", + "server_down": "Immich-servern är för närvarande nere eller kan inte nås", + "server_url": "Immich Server URL", + "update_integration": "Uppdatera integration" } } diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 0a686a2a..4444be97 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -215,7 +215,8 @@ "start": "开始", "starting_airport": "出发机场", "to": "到", - "transportation_delete_confirm": "您确定要删除此交通工具吗?\n此操作无法撤消。" + "transportation_delete_confirm": "您确定要删除此交通工具吗?\n此操作无法撤消。", + "show_map": "显示地图" }, "home": { "desc_1": "轻松发现、规划和探索", @@ -500,5 +501,28 @@ "total_adventures": "全面冒险", "total_visited_regions": "总访问地区", "welcome_back": "欢迎回来" + }, + "immich": { + "api_key": "伊米奇 API 密钥", + "api_note": "注意:这必须是 Immich API 服务器的 URL,因此它可能以 /api 结尾,除非您有自定义配置。", + "disable": "禁用", + "enable_immich": "启用伊米奇", + "imageid_required": "需要图像 ID", + "immich": "伊米奇", + "immich_desc": "将您的 Immich 帐户与 AdventureLog 集成,以便您搜索照片库并导入冒险照片。", + "immich_disabled": "Immich 集成成功禁用!", + "immich_enabled": "Immich 集成成功启用!", + "immich_error": "更新 Immich 集成时出错", + "immich_integration": "伊米奇整合", + "immich_updated": "Immich 设置更新成功!", + "integration_enabled": "启用集成", + "integration_fetch_error": "从 Immich 集成获取数据时出错", + "integration_missing": "后端缺少 Immich 集成", + "load_more": "加载更多", + "no_items_found": "没有找到物品", + "query_required": "需要查询", + "server_down": "Immich 服务器当前已关闭或无法访问", + "server_url": "伊米奇服务器网址", + "update_integration": "更新集成" } } diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 800084a2..5b2c305d 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -152,10 +152,10 @@ }); let data = await res.json(); if (res.ok) { - addToast('success', $t('settings.immich_enabled')); + addToast('success', $t('immich.immich_enabled')); immichIntegration = data; } else { - addToast('error', $t('settings.immich_error')); + addToast('error', $t('immich.immich_error')); } } else { let res = await fetch(`/api/integrations/immich/${immichIntegration.id}/`, { @@ -167,10 +167,10 @@ }); let data = await res.json(); if (res.ok) { - addToast('success', $t('settings.immich_updated')); + addToast('success', $t('immich.immich_updated')); immichIntegration = data; } else { - addToast('error', $t('settings.immich_error')); + addToast('error', $t('immich.immich_error')); } } } @@ -181,10 +181,10 @@ method: 'DELETE' }); if (res.ok) { - addToast('success', $t('settings.immich_disabled')); + addToast('success', $t('immich.immich_disabled')); immichIntegration = null; } else { - addToast('error', $t('settings.immich_error')); + addToast('error', $t('immich.immich_error')); } } } @@ -427,20 +427,16 @@

    - Immich Integration Immich Logo + {$t('immich.immich_integration')} + Immich

    - Integrate your Immich account with AdventureLog to allow you to search your photos library - and import photos for your adventures. + {$t('immich.immich_desc')}

    {#if immichIntegration}
    -
    Integration Enabled
    +
    {$t('immich.integration_enabled')}
    - +
    {/if} @@ -456,38 +454,39 @@
    {$t('immich.server_url')} {#if newImmichIntegration.server_url && !newImmichIntegration.server_url.endsWith('api')}

    - Note: this must be the URL to the Immich API server so it likely ends with /api - unless you have a custom config. + {$t('immich.api_note')}

    {/if}
    {$t('immich.api_key')}
    {!immichIntegration?.id + ? $t('immich.enable_immich') + : $t('immich.update_integration')}
    {/if} From 73036dcef849d90525019334c0de3fb31a6aad7a Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 2 Jan 2025 18:38:29 -0500 Subject: [PATCH 09/24] fix: put languages in locale selection dropdown into native language --- frontend/src/lib/components/Navbar.svelte | 16 ++++++++++++++-- frontend/src/locales/de.json | 12 +----------- frontend/src/locales/en.json | 12 +----------- frontend/src/locales/es.json | 12 +----------- frontend/src/locales/fr.json | 12 +----------- frontend/src/locales/it.json | 12 +----------- frontend/src/locales/nl.json | 12 +----------- frontend/src/locales/pl.json | 12 +----------- frontend/src/locales/sv.json | 12 +----------- frontend/src/locales/zh.json | 12 +----------- 10 files changed, 23 insertions(+), 101 deletions(-) diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index fbe83374..bbfbaf5f 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -13,6 +13,18 @@ import { t, locale, locales } from 'svelte-i18n'; import { themes } from '$lib'; + let languages: { [key: string]: string } = { + en: 'English', + de: 'Deutsch', + es: 'Español', + fr: 'Français', + it: 'Italiano', + nl: 'Nederlands', + sv: 'Svenska', + zh: '中文', + pl: 'Polski' + }; + let query: string = ''; let isAboutModalOpen: boolean = false; @@ -236,8 +248,8 @@ on:change={submitLocaleChange} bind:value={$locale} > - {#each $locales as loc} - + {#each $locales as loc (loc)} + {/each} diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 09043920..a8edb0d6 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -455,17 +455,7 @@ "show_visited_regions": "Besuchte Regionen anzeigen", "view_details": "Details anzeigen" }, - "languages": { - "de": "Deutsch", - "en": "Englisch", - "es": "Spanisch", - "fr": "Französisch", - "it": "Italienisch", - "nl": "Niederländisch", - "sv": "Schwedisch", - "zh": "chinesisch", - "pl": "Polnisch" - }, + "languages": {}, "share": { "no_users_shared": "Keine Benutzer geteilt mit", "not_shared_with": "Nicht geteilt mit", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 2acebc9e..d64bdaa2 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -467,17 +467,7 @@ "set_public": "In order to allow users to share with you, you need your profile set to public.", "go_to_settings": "Go to settings" }, - "languages": { - "en": "English", - "de": "German", - "es": "Spanish", - "fr": "French", - "it": "Italian", - "nl": "Dutch", - "sv": "Swedish", - "zh": "Chinese", - "pl": "Polish" - }, + "languages": {}, "profile": { "member_since": "Member since", "user_stats": "User Stats", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index aa23a71b..a66b5ca0 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -467,17 +467,7 @@ "no_shared_found": "No se encontraron colecciones que se compartan contigo.", "set_public": "Para permitir que los usuarios compartan contenido con usted, necesita que su perfil esté configurado como público." }, - "languages": { - "de": "Alemán", - "en": "Inglés", - "es": "Español", - "fr": "Francés", - "it": "italiano", - "nl": "Holandés", - "sv": "sueco", - "zh": "Chino", - "pl": "Polaco" - }, + "languages": {}, "profile": { "member_since": "Miembro desde", "user_stats": "Estadísticas de usuario", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index eaa8842e..a6aa22df 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -455,17 +455,7 @@ "show_visited_regions": "Afficher les régions visitées", "view_details": "Afficher les détails" }, - "languages": { - "de": "Allemand", - "en": "Anglais", - "es": "Espagnol", - "fr": "Français", - "it": "italien", - "nl": "Néerlandais", - "sv": "suédois", - "zh": "Chinois", - "pl": "Polonais" - }, + "languages": {}, "share": { "no_users_shared": "Aucun utilisateur partagé avec", "not_shared_with": "Non partagé avec", diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 0b880a93..8f93cdd4 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -455,17 +455,7 @@ "show_visited_regions": "Mostra regioni visitate", "view_details": "Visualizza dettagli" }, - "languages": { - "de": "tedesco", - "en": "Inglese", - "es": "spagnolo", - "fr": "francese", - "it": "Italiano", - "nl": "Olandese", - "sv": "svedese", - "zh": "cinese", - "pl": "Polacco" - }, + "languages": {}, "share": { "no_users_shared": "Nessun utente condiviso con", "not_shared_with": "Non condiviso con", diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index f00fac4d..149babae 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -455,17 +455,7 @@ "show_visited_regions": "Toon bezochte regio's", "view_details": "Details bekijken" }, - "languages": { - "de": "Duits", - "en": "Engels", - "es": "Spaans", - "fr": "Frans", - "it": "Italiaans", - "nl": "Nederlands", - "sv": "Zweeds", - "zh": "Chinese", - "pl": "Pools" - }, + "languages": {}, "share": { "no_users_shared": "Er zijn geen gebruikers gedeeld", "not_shared_with": "Niet gedeeld met", diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index b9a13cfb..3e369b9e 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -467,17 +467,7 @@ "set_public": "Aby umożliwić użytkownikom udostępnianie Tobie, musisz ustawić swój profil jako publiczny.", "go_to_settings": "Przejdź do ustawień" }, - "languages": { - "en": "Angielski", - "de": "Niemiecki", - "es": "Hiszpański", - "fr": "Francuski", - "it": "Włoski", - "nl": "Holenderski", - "sv": "Szwedzki", - "zh": "Chiński", - "pl": "Polski" - }, + "languages": {}, "profile": { "member_since": "Użytkownik od", "user_stats": "Statystyki użytkownika", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index a4c443ca..214393c5 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -455,17 +455,7 @@ "show_visited_regions": "Visa besökta regioner", "view_details": "Visa detaljer" }, - "languages": { - "de": "tyska", - "en": "engelska", - "es": "spanska", - "fr": "franska", - "it": "italienska", - "nl": "holländska", - "sv": "svenska", - "zh": "kinesiska", - "pl": "polska" - }, + "languages": {}, "share": { "no_users_shared": "Inga användare delas med", "not_shared_with": "Inte delad med", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 4444be97..f9ee9071 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -455,17 +455,7 @@ "show_visited_regions": "显示访问过的地区", "view_details": "查看详情" }, - "languages": { - "de": "德语", - "en": "英语", - "es": "西班牙语", - "fr": "法语", - "it": "意大利语", - "nl": "荷兰语", - "sv": "瑞典", - "zh": "中国人", - "pl": "波兰语" - }, + "languages": {}, "share": { "no_users_shared": "没有与之共享的用户", "not_shared_with": "不与共享", From 991efa8d08eeb4328dc5dd02c45b9bf23f94c600 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 2 Jan 2025 19:00:11 -0500 Subject: [PATCH 10/24] docs: add Immich integration documentation and update configuration menu --- documentation/.vitepress/config.mts | 4 +++ .../docs/configuration/immich_integration.md | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 documentation/docs/configuration/immich_integration.md diff --git a/documentation/.vitepress/config.mts b/documentation/.vitepress/config.mts index 1c47f293..72316c3a 100644 --- a/documentation/.vitepress/config.mts +++ b/documentation/.vitepress/config.mts @@ -87,6 +87,10 @@ export default defineConfig({ text: "Configuration", collapsed: false, items: [ + { + text: "Immich Integration", + link: "/docs/configuration/immich_integration", + }, { text: "Update App", link: "/docs/configuration/updating", diff --git a/documentation/docs/configuration/immich_integration.md b/documentation/docs/configuration/immich_integration.md new file mode 100644 index 00000000..117fd31f --- /dev/null +++ b/documentation/docs/configuration/immich_integration.md @@ -0,0 +1,28 @@ +# Immich Integration + +### What is Immich? + + + +![Immich Banner](https://repository-images.githubusercontent.com/455229168/ebba3238-9ef5-4891-ad58-a3b0223b12bd) + +Immich is a self-hosted, open-source platform that allows users to backup and manage their photos and videos similar to Google Photos, but with the advantage of storing data on their own private server, ensuring greater privacy and control over their media. + +- [Immich Website and Documentation](https://immich.app/) +- [GitHub Repository](https://github.com/immich-app/immich) + +### How to integrate Immich with AdventureLog? + +To integrate Immich with AdventureLog, you need to have an Immich server running and accessible from where AdventureLog is running. + +1. Obtain the Immich API Key from the Immich server. + - In the Immich web interface, click on your user profile picture, go to `Account Settings` > `API Keys`. + - Click `New API Key` and name it something like `AdventureLog`. + - Copy the generated API Key, you will need it in the next step. +2. Go to the AdventureLog web interface, click on your user profile picture, go to `Settings` and scroll down to the `Immich Integration` section. + - Enter the URL of your Immich server, e.g. `https://immich.example.com`. + - Paste the API Key you obtained in the previous step. + - Click `Enable Immich` to save the settings. +3. Now, when you are adding images to an adventure, you will see an option to search for images in Immich or upload from an album. + +Enjoy the privacy and control of managing your travel media with Immich and AdventureLog! 🎉 From c6fa603a93e708a4ba3cb04db9c837f9c8456947 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Thu, 2 Jan 2025 23:25:58 -0500 Subject: [PATCH 11/24] feat: add primary image functionality to AdventureImage model and update related components --- .../0017_adventureimage_is_primary.py | 18 ++++++++ backend/server/adventures/models.py | 1 + backend/server/adventures/serializers.py | 2 +- backend/server/adventures/views.py | 22 ++++++++++ .../src/lib/components/AdventureModal.svelte | 44 +++++++++++++++++-- .../src/lib/components/CardCarousel.svelte | 19 +++++++- frontend/src/lib/types.ts | 1 + .../src/routes/adventures/[id]/+page.svelte | 10 +++++ 8 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 backend/server/adventures/migrations/0017_adventureimage_is_primary.py diff --git a/backend/server/adventures/migrations/0017_adventureimage_is_primary.py b/backend/server/adventures/migrations/0017_adventureimage_is_primary.py new file mode 100644 index 00000000..9a920a39 --- /dev/null +++ b/backend/server/adventures/migrations/0017_adventureimage_is_primary.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.8 on 2025-01-03 04:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0016_alter_adventureimage_image'), + ] + + operations = [ + migrations.AddField( + model_name='adventureimage', + name='is_primary', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index 68c8ad66..c77bc4dd 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -280,6 +280,7 @@ class AdventureImage(models.Model): upload_to=PathAndRename('images/') # Use the callable class here ) adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE) + is_primary = models.BooleanField(default=False) def __str__(self): return self.image.url diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index 45a2141b..d78e93d3 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -8,7 +8,7 @@ class AdventureImageSerializer(CustomModelSerializer): class Meta: model = AdventureImage - fields = ['id', 'image', 'adventure'] + fields = ['id', 'image', 'adventure', 'is_primary'] read_only_fields = ['id'] def to_representation(self, instance): diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index b1c39561..3ee3e793 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -1032,7 +1032,29 @@ def dispatch(self, request, *args, **kwargs): @action(detail=True, methods=['post']) def image_delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) + + @action(detail=True, methods=['post']) + def toggle_primary(self, request, *args, **kwargs): + # Makes the image the primary image for the adventure, if there is already a primary image linked to the adventure, it is set to false and the new image is set to true. make sure that the permission is set to the owner of the adventure + if not request.user.is_authenticated: + return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED) + + instance = self.get_object() + adventure = instance.adventure + if adventure.user_id != request.user: + return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN) + + # Check if the image is already the primary image + if instance.is_primary: + return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST) + + # Set the current primary image to false + AdventureImage.objects.filter(adventure=adventure, is_primary=True).update(is_primary=False) + # Set the new image to true + instance.is_primary = True + instance.save() + return Response({"success": "Image set as primary image"}) def create(self, request, *args, **kwargs): if not request.user.is_authenticated: diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 550c4194..138ff3d0 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -21,7 +21,7 @@ let query: string = ''; let places: OpenStreetMapPlace[] = []; - let images: { id: string; image: string }[] = []; + let images: { id: string; image: string; is_primary: boolean }[] = []; let warningMessage: string = ''; let constrainDates: boolean = false; @@ -34,6 +34,9 @@ import MarkdownEditor from './MarkdownEditor.svelte'; import ImmichSelect from './ImmichSelect.svelte'; + import Star from '~icons/mdi/star'; + import Crown from '~icons/mdi/crown'; + let wikiError: string = ''; let noPlaces: boolean = false; @@ -179,6 +182,25 @@ } } + async function makePrimaryImage(image_id: string) { + let res = await fetch(`/api/images/${image_id}/toggle_primary`, { + method: 'POST' + }); + if (res.ok) { + images = images.map((image) => { + if (image.id === image_id) { + image.is_primary = true; + } else { + image.is_primary = false; + } + return image; + }); + adventure.images = images; + } else { + console.error('Error in makePrimaryImage:', res); + } + } + async function fetchImage() { try { let res = await fetch(url); @@ -208,7 +230,8 @@ // Assuming the first object in the array is the new image let newImage = { id: rawData[1], - image: rawData[2] // This is the URL for the image + image: rawData[2], // This is the URL for the image + is_primary: false }; console.log('New Image:', newImage); @@ -249,7 +272,7 @@ if (res2.ok) { let newData = deserialize(await res2.text()) as { data: { id: string; image: string } }; console.log(newData); - let newImage = { id: newData.data.id, image: newData.data.image }; + let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false }; console.log(newImage); images = [...images, newImage]; adventure.images = images; @@ -1038,6 +1061,21 @@ it would also work to just use on:click on the MapLibre component itself. --> > ✕ + {#if !image.is_primary} + + {:else} + + +
    + +
    + {/if} {image.id} - adventure.images.map((image) => ({ image: image.image, adventure: adventure })) + adventure.images.map((image) => ({ + image: image.image, + adventure: adventure, + is_primary: image.is_primary + })) ); $: { @@ -18,6 +22,19 @@ } } + $: { + // sort so that any image in adventure_images .is_primary is first + adventure_images.sort((a, b) => { + if (a.is_primary && !b.is_primary) { + return -1; + } else if (!a.is_primary && b.is_primary) { + return 1; + } else { + return 0; + } + }); + } + function changeSlide(direction: string) { if (direction === 'next' && currentSlide < adventure_images.length - 1) { currentSlide = currentSlide + 1; diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index a2e6d3ca..ea9e1fb3 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -24,6 +24,7 @@ export type Adventure = { images: { id: string; image: string; + is_primary: boolean; }[]; visits: { id: string; diff --git a/frontend/src/routes/adventures/[id]/+page.svelte b/frontend/src/routes/adventures/[id]/+page.svelte index f151bb4a..21b622f4 100644 --- a/frontend/src/routes/adventures/[id]/+page.svelte +++ b/frontend/src/routes/adventures/[id]/+page.svelte @@ -34,6 +34,16 @@ onMount(() => { if (data.props.adventure) { adventure = data.props.adventure; + // sort so that any image in adventure_images .is_primary is first + adventure.images.sort((a, b) => { + if (a.is_primary && !b.is_primary) { + return -1; + } else if (!a.is_primary && b.is_primary) { + return 1; + } else { + return 0; + } + }); } else { notFound = true; } From 6651557738fe9f1ab796ca776dc37906eb3e66fc Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 09:47:05 -0500 Subject: [PATCH 12/24] feat: include adventure visits in collection update requests --- frontend/src/lib/components/AdventureCard.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index b77b8eaa..49990d55 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -80,7 +80,7 @@ headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ collection: null }) + body: JSON.stringify({ collection: null, visits: adventure.visits }) }); if (res.ok) { addToast('info', `${$t('adventures.collection_remove_success')}`); @@ -97,7 +97,7 @@ headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ collection: collectionId }) + body: JSON.stringify({ collection: collectionId, visits: adventure.visits }) }); if (res.ok) { console.log('Adventure linked to collection'); From 57e367d112def03a3c2bda086b055b2e5ecb201b Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 09:53:23 -0500 Subject: [PATCH 13/24] refactor: update AdventureSerializer to handle visits data more gracefully and remove visits from request body in AdventureCard --- backend/server/adventures/serializers.py | 36 ++++++++++--------- .../src/lib/components/AdventureCard.svelte | 4 +-- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index d78e93d3..2c677f73 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -116,7 +116,7 @@ def get_is_visited(self, obj): return False def create(self, validated_data): - visits_data = validated_data.pop('visits', []) + visits_data = validated_data.pop('visits', None) category_data = validated_data.pop('category', None) print(category_data) adventure = Adventure.objects.create(**validated_data) @@ -131,6 +131,7 @@ def create(self, validated_data): return adventure def update(self, instance, validated_data): + has_visits = 'visits' in validated_data visits_data = validated_data.pop('visits', []) category_data = validated_data.pop('category', None) @@ -142,24 +143,25 @@ def update(self, instance, validated_data): instance.category = category instance.save() - current_visits = instance.visits.all() - current_visit_ids = set(current_visits.values_list('id', flat=True)) + if has_visits: + current_visits = instance.visits.all() + current_visit_ids = set(current_visits.values_list('id', flat=True)) - updated_visit_ids = set() - for visit_data in visits_data: - visit_id = visit_data.get('id') - if visit_id and visit_id in current_visit_ids: - visit = current_visits.get(id=visit_id) - for attr, value in visit_data.items(): - setattr(visit, attr, value) - visit.save() - updated_visit_ids.add(visit_id) - else: - new_visit = Visit.objects.create(adventure=instance, **visit_data) - updated_visit_ids.add(new_visit.id) + updated_visit_ids = set() + for visit_data in visits_data: + visit_id = visit_data.get('id') + if visit_id and visit_id in current_visit_ids: + visit = current_visits.get(id=visit_id) + for attr, value in visit_data.items(): + setattr(visit, attr, value) + visit.save() + updated_visit_ids.add(visit_id) + else: + new_visit = Visit.objects.create(adventure=instance, **visit_data) + updated_visit_ids.add(new_visit.id) - visits_to_delete = current_visit_ids - updated_visit_ids - instance.visits.filter(id__in=visits_to_delete).delete() + visits_to_delete = current_visit_ids - updated_visit_ids + instance.visits.filter(id__in=visits_to_delete).delete() return instance diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 49990d55..b77b8eaa 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -80,7 +80,7 @@ headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ collection: null, visits: adventure.visits }) + body: JSON.stringify({ collection: null }) }); if (res.ok) { addToast('info', `${$t('adventures.collection_remove_success')}`); @@ -97,7 +97,7 @@ headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ collection: collectionId, visits: adventure.visits }) + body: JSON.stringify({ collection: collectionId }) }); if (res.ok) { console.log('Adventure linked to collection'); From 3a024e1e18b844cbd587ebb3277f4bcde2b25399 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 12:05:02 -0500 Subject: [PATCH 14/24] feat: implement logic to determine if an adventure will be marked as visited based on visit dates --- .../src/lib/components/AdventureModal.svelte | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 138ff3d0..584f72d0 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -164,6 +164,33 @@ close(); } + let willBeMarkedVisited: boolean = false; + + $: { + willBeMarkedVisited = false; // Reset before evaluating + + const today = new Date(); // Cache today's date to avoid redundant calculations + + for (const visit of adventure.visits) { + const startDate = new Date(visit.start_date); + const endDate = visit.end_date ? new Date(visit.end_date) : null; + + // If the visit has both a start date and an end date, check if it started by today + if (startDate && endDate && startDate <= today) { + willBeMarkedVisited = true; + break; // Exit the loop since we've determined the result + } + + // If the visit has a start date but no end date, check if it started by today + if (startDate && !endDate && startDate <= today) { + willBeMarkedVisited = true; + break; // Exit the loop since we've determined the result + } + } + + console.log('WMBV:', willBeMarkedVisited); + } + let previousCoords: { lat: number; lng: number } | null = null; $: if (markers.length > 0) { @@ -515,6 +542,9 @@ addToast('error', $t('adventures.adventure_update_error')); } } + if (adventure.is_visited) { + markVisited(); + } } @@ -761,7 +791,12 @@ it would also work to just use on:click on the MapLibre component itself. --> : $t('adventures.not_visited')}

    - {#if !reverseGeocodePlace.is_visited} + {#if !reverseGeocodePlace.is_visited && !willBeMarkedVisited} + + {/if} + {#if !reverseGeocodePlace.is_visited && willBeMarkedVisited} {/if} {/if} From 50e0d4a34e5b2bcd35420fb89da33f29cf13f9fd Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 12:12:09 -0500 Subject: [PATCH 15/24] feat: add localization for adventure visit confirmation message in multiple languages --- frontend/src/lib/components/AdventureModal.svelte | 4 ++-- frontend/src/locales/de.json | 3 ++- frontend/src/locales/en.json | 1 + frontend/src/locales/es.json | 3 ++- frontend/src/locales/fr.json | 3 ++- frontend/src/locales/it.json | 3 ++- frontend/src/locales/nl.json | 3 ++- frontend/src/locales/pl.json | 3 ++- frontend/src/locales/sv.json | 3 ++- frontend/src/locales/zh.json | 3 ++- 10 files changed, 19 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 584f72d0..89e21220 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -813,8 +813,8 @@ it would also work to just use on:click on the MapLibre component itself. --> {reverseGeocodePlace.region}, - {reverseGeocodePlace.country} will be marked as visited once the adventure is - saved.
    {/if} diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index a8edb0d6..3bbd4259 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -216,7 +216,8 @@ "starting_airport": "Startflughafen", "to": "Zu", "transportation_delete_confirm": "Sind Sie sicher, dass Sie diesen Transport löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.", - "show_map": "Karte anzeigen" + "show_map": "Karte anzeigen", + "will_be_marked": "wird als besucht markiert, sobald das Abenteuer gespeichert ist." }, "home": { "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index d64bdaa2..9aaa4b23 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -229,6 +229,7 @@ "no_location_found": "No location found", "from": "From", "to": "To", + "will_be_marked": "will be marked as visited once the adventure is saved.", "start": "Start", "end": "End", "show_map": "Show Map", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index a66b5ca0..11f54966 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -263,7 +263,8 @@ "starting_airport": "Aeropuerto de inicio", "to": "A", "transportation_delete_confirm": "¿Está seguro de que desea eliminar este transporte? \nEsta acción no se puede deshacer.", - "show_map": "Mostrar mapa" + "show_map": "Mostrar mapa", + "will_be_marked": "se marcará como visitado una vez guardada la aventura." }, "worldtravel": { "all": "Todo", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index a6aa22df..e6f7573f 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -216,7 +216,8 @@ "starting_airport": "Aéroport de départ", "to": "À", "transportation_delete_confirm": "Etes-vous sûr de vouloir supprimer ce transport ? \nCette action ne peut pas être annulée.", - "show_map": "Afficher la carte" + "show_map": "Afficher la carte", + "will_be_marked": "sera marqué comme visité une fois l’aventure sauvegardée." }, "home": { "desc_1": "Découvrez, planifiez et explorez en toute simplicité", diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 8f93cdd4..49b43d9c 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -216,7 +216,8 @@ "starting_airport": "Inizio aeroporto", "to": "A", "transportation_delete_confirm": "Sei sicuro di voler eliminare questo trasporto? \nQuesta azione non può essere annullata.", - "show_map": "Mostra mappa" + "show_map": "Mostra mappa", + "will_be_marked": "verrà contrassegnato come visitato una volta salvata l'avventura." }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index 149babae..c207f8a1 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -216,7 +216,8 @@ "to": "Naar", "transportation_delete_confirm": "Weet u zeker dat u dit transport wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.", "ending_airport": "Einde luchthaven", - "show_map": "Toon kaart" + "show_map": "Toon kaart", + "will_be_marked": "wordt gemarkeerd als bezocht zodra het avontuur is opgeslagen." }, "home": { "desc_1": "Ontdek, plan en verken met gemak", diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index 3e369b9e..73361d99 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -263,7 +263,8 @@ "starting_airport": "Początkowe lotnisko", "to": "Do", "transportation_delete_confirm": "Czy na pewno chcesz usunąć ten transport? \nTej akcji nie można cofnąć.", - "show_map": "Pokaż mapę" + "show_map": "Pokaż mapę", + "will_be_marked": "zostanie oznaczona jako odwiedzona po zapisaniu przygody." }, "worldtravel": { "country_list": "Lista krajów", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 214393c5..ee195bb2 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -216,7 +216,8 @@ "starting_airport": "Startar flygplats", "to": "Till", "transportation_delete_confirm": "Är du säker på att du vill ta bort denna transport? \nDenna åtgärd kan inte ångras.", - "show_map": "Visa karta" + "show_map": "Visa karta", + "will_be_marked": "kommer att markeras som besökt när äventyret har sparats." }, "home": { "desc_1": "Upptäck, planera och utforska med lätthet", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index f9ee9071..659dd08d 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -216,7 +216,8 @@ "starting_airport": "出发机场", "to": "到", "transportation_delete_confirm": "您确定要删除此交通工具吗?\n此操作无法撤消。", - "show_map": "显示地图" + "show_map": "显示地图", + "will_be_marked": "保存冒险后将被标记为已访问。" }, "home": { "desc_1": "轻松发现、规划和探索", From 82a1134019f959c1bee688a7d57f69a15c181f12 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 12:15:39 -0500 Subject: [PATCH 16/24] fix: prevent marking adventure as visited if the place is already visited --- frontend/src/lib/components/AdventureModal.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 89e21220..1d1b2c06 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -542,7 +542,7 @@ addToast('error', $t('adventures.adventure_update_error')); } } - if (adventure.is_visited) { + if (adventure.is_visited && !reverseGeocodePlace?.is_visited) { markVisited(); } } From 259f2e75c2e57130e5786c7c78e69822d552dbb8 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 12:24:17 -0500 Subject: [PATCH 17/24] feat: version bump for release v0.8.0 --- documentation/.vitepress/config.mts | 4 + documentation/docs/changelogs/v0-8-0.md | 105 ++++++++++++++++++++++++ frontend/package.json | 2 +- frontend/src/lib/config.ts | 4 +- 4 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 documentation/docs/changelogs/v0-8-0.md diff --git a/documentation/.vitepress/config.mts b/documentation/.vitepress/config.mts index 72316c3a..4f1264c6 100644 --- a/documentation/.vitepress/config.mts +++ b/documentation/.vitepress/config.mts @@ -135,6 +135,10 @@ export default defineConfig({ text: "Changelogs", collapsed: false, items: [ + { + text: "v0.8.0", + link: "/docs/changelogs/v0-8-0", + }, { text: "v0.7.1", link: "/docs/changelogs/v0-7-1", diff --git a/documentation/docs/changelogs/v0-8-0.md b/documentation/docs/changelogs/v0-8-0.md new file mode 100644 index 00000000..48d9c18e --- /dev/null +++ b/documentation/docs/changelogs/v0-8-0.md @@ -0,0 +1,105 @@ +# AdventureLog v0.8.0 - Immich Integration, Calendar and Customization + +Released 01-08-2025 + +Hi everyone! 🚀 +I’m thrilled to announce the release of **AdventureLog v0.8.0**, a huge update packed with new features, improvements, and enhancements. This release focuses on delivering a better user experience, improved functionality, and expanded customization options. Let’s dive into what’s new! + +--- + +## What's New ✨ + +### Immich Integration + +- AdventureLog now integrates seamlessly with [Immich](https://github.com/immich-app), the amazing self-hostable photo library. +- Import your photos from Immich directly into AdventureLog adventures and collections. + - Use Immich Smart Search to search images to import based on natural queries. + - Sort by photo album to easily import your trips photos to an adventure. + +### 🚗 Transportation + +- **New Transportation Edit Modal**: Includes detailed origin and destination location information for better trip planning. +- **Autocomplete for Airport Codes**: Quickly find and add airport codes while planning transportations. +- **New Transportation Card Design**: Redesigned for better clarity and aesthetics. + +--- + +### 📝 Notes and Checklists + +- **New Modals for Notes and Checklists**: Simplified creation and editing of your notes and checklists. +- **Delete Confirmation**: Added a confirmation step when deleting notes, checklists, or transportations to prevent accidental deletions. + +--- + +### 📍Adventures + +- **Markdown Editor and Preview**: Write and format adventure descriptions with a markdown editor. +- **Custom Categories**: Organize your adventures with personalized categories and icons. +- **Primary Images**: Adventure images can now be marked as the "primary image" and will be the first one to be displayed in adventure views. + +--- + +### 🗓️ Calendar + +- **Calendar View**: View your adventures and transportations in a calendar layout. +- **ICS File Export**: Export your calendar as an ICS file for use with external apps like Google Calendar or Outlook. + +--- + +### 🌐 Localization + +- Added support for **Polish** language (@dymek37). +- Improved Swedish language data (@nordtechtiger) + +--- + +### 🔒 Authentication + +- **New Authentication System**: Includes MFA for added security. +- **Admin Page Authentication**: Enhanced protection for admin operations. + > [!IMPORTANT] + > Ensure you know your credentials as you will be signed out after updating! + +--- + +### 🖌️ UI & Theming + +- **Nord Theme**: A sleek new theme option for a modern and clean interface. +- **New Home Dashboard**: A revamped dashboard experience to access everything you need quickly and view your travel stats. + +--- + +### ⚙️ Settings + +- **Overhauled Settings Page**: Redesigned for better navigation and usability. + +--- + +### 🐛 Bug Fixes and Improvements + +- Fixed the **NGINX Upload Size Bug**: Upload larger files without issues. +- **Prevents Duplicate Emails**: Improved account management; users can now add multiple emails to a single account. +- General **code cleanliness** for better performance and stability. +- Fixes Django Admin access through Traefik (@PascalBru) + +--- + +### 🌐 Infrastructure + +- Added **Kubernetes Configurations** for scalable deployments (@MaximUltimatum). +- Launched a **New [Documentation Site](https://adventurelog.app)** for better guidance and support. + +--- + +## Sponsorship 💖 + +[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/seanmorley15) +As always, AdventureLog continues to grow thanks to your incredible support and feedback. If you love using the app and want to help shape its future, consider supporting me on **Buy Me A Coffee**. Your contributions go a long way in allowing for AdventureLog to continue to improve and thrive 😊 + +--- + +Enjoy the update! 🎉 +Feel free to share your feedback, ideas, or questions in the discussion below or on the official [discord server](https://discord.gg/wRbQ9Egr8C)! + +Happy travels, +**Sean Morley** (@seanmorley15) diff --git a/frontend/package.json b/frontend/package.json index e2b9a261..442a9d6a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "adventurelog-frontend", - "version": "0.7.1", + "version": "0.8.0", "scripts": { "dev": "vite dev", "django": "cd .. && cd backend/server && python3 manage.py runserver", diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index 3b9fde7e..d2015826 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -1,4 +1,4 @@ -export let appVersion = 'v0.7.1'; -export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.7.1'; +export let appVersion = 'v0.8.0'; +export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.8.0'; export let appTitle = 'AdventureLog'; export let copyrightYear = '2023-2025'; From 230a786d6eeb15e6b2261670e730e8adb949445e Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 15:58:52 -0500 Subject: [PATCH 18/24] docs: add instructions for customizing email subject in admin site --- documentation/docs/configuration/email.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/documentation/docs/configuration/email.md b/documentation/docs/configuration/email.md index f3fb3130..53129105 100644 --- a/documentation/docs/configuration/email.md +++ b/documentation/docs/configuration/email.md @@ -22,3 +22,13 @@ environment: - EMAIL_HOST_PASSWORD='password' - DEFAULT_FROM_EMAIL='user@example.com' ``` + +## Customizing Emails + +By default, the email will display `[example.com]` in the subject. You can customize this in the admin site. + +1. Go to the admin site (serverurl/admin) +2. Click on `Sites` +3. Click on first site, it will probably be `example.com` +4. Change the `Domain name` and `Display name` to your desired values +5. Click `Save` From 56244329f52fd55f0ac7502848871df43e107146 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 16:29:39 -0500 Subject: [PATCH 19/24] feat: configure REST framework renderers based on DEBUG setting --- backend/server/main/settings.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index b934b990..5acecb74 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -220,6 +220,17 @@ 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', } +if DEBUG: + REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = ( + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ) +else: + REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = ( + 'rest_framework.renderers.JSONRenderer', + ) + + CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()] From a4df852744ad2f20270e11c90053e27de7c34793 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 16:42:27 -0500 Subject: [PATCH 20/24] feat: update .env.example for demo database setup and add image search functionality in AdventureModal --- backend/server/.env.example | 13 ++++++++++++- frontend/src/lib/components/AdventureModal.svelte | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/server/.env.example b/backend/server/.env.example index 04eb77f1..4c1f9ad3 100644 --- a/backend/server/.env.example +++ b/backend/server/.env.example @@ -20,4 +20,15 @@ EMAIL_BACKEND='console' # EMAIL_USE_SSL=True # EMAIL_HOST_USER='user' # EMAIL_HOST_PASSWORD='password' -# DEFAULT_FROM_EMAIL='user@example.com' \ No newline at end of file +# DEFAULT_FROM_EMAIL='user@example.com' + + +# ------------------- # +# For Developers to start a Demo Database +# docker run --name postgres-admin -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=admin -p 5432:5432 -d postgis/postgis:15-3.3 + +# PGHOST='localhost' +# PGDATABASE='admin' +# PGUSER='admin' +# PGPASSWORD='admin' +# ------------------- # \ No newline at end of file diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 1d1b2c06..eda9df6d 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -545,6 +545,7 @@ if (adventure.is_visited && !reverseGeocodePlace?.is_visited) { markVisited(); } + imageSearch = adventure.name; } From 9e4846a66a34ecf11c19f6c6ad1b4c62fb3ecb62 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 3 Jan 2025 18:46:45 -0500 Subject: [PATCH 21/24] feat: enhance Immich integration documentation and add warnings for localhost usage --- documentation/docs/configuration/immich_integration.md | 2 +- frontend/src/lib/components/NoteModal.svelte | 2 +- frontend/src/lib/index.ts | 2 +- frontend/src/locales/en.json | 4 +++- frontend/src/routes/settings/+page.svelte | 10 ++++++++++ 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/documentation/docs/configuration/immich_integration.md b/documentation/docs/configuration/immich_integration.md index 117fd31f..a8b2ae15 100644 --- a/documentation/docs/configuration/immich_integration.md +++ b/documentation/docs/configuration/immich_integration.md @@ -20,7 +20,7 @@ To integrate Immich with AdventureLog, you need to have an Immich server running - Click `New API Key` and name it something like `AdventureLog`. - Copy the generated API Key, you will need it in the next step. 2. Go to the AdventureLog web interface, click on your user profile picture, go to `Settings` and scroll down to the `Immich Integration` section. - - Enter the URL of your Immich server, e.g. `https://immich.example.com`. + - Enter the URL of your Immich server, e.g. `https://immich.example.com`. Note that `localhost` or `127.0.0.1` will probably not work because Immich and AdventureLog are running on different docker networks. It is recommended to use the IP address of the server where Immich is running ex `http://my-server-ip:port` or a domain name. - Paste the API Key you obtained in the previous step. - Click `Enable Immich` to save the settings. 3. Now, when you are adding images to an adventure, you will see an option to search for images in Immich or upload from an album. diff --git a/frontend/src/lib/components/NoteModal.svelte b/frontend/src/lib/components/NoteModal.svelte index 83449849..a895f336 100644 --- a/frontend/src/lib/components/NoteModal.svelte +++ b/frontend/src/lib/components/NoteModal.svelte @@ -188,7 +188,7 @@

    {#if !isReadOnly} - + {:else if note}

    = newYearsStart && today <= newYearsEnd) { return { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 9aaa4b23..a7e668e8 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -514,6 +514,8 @@ "api_key": "Immich API Key", "enable_immich": "Enable Immich", "update_integration": "Update Integration", - "immich_integration": "Immich Integration" + "immich_integration": "Immich Integration", + "localhost_note": "Note: localhost will most likely not work unless you have setup docker networks accordingly. It is recommended to use the IP address of the server or the domain name.", + "documentation": "Immich Integration Documentation" } } diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 5b2c305d..6c2b97f9 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -433,6 +433,11 @@

    {$t('immich.immich_desc')} + {$t('immich.documentation')}

    {#if immichIntegration}
    @@ -469,6 +474,11 @@ {$t('immich.api_note')}

    {/if} + {#if newImmichIntegration.server_url && (newImmichIntegration.server_url.indexOf('localhost') !== -1 || newImmichIntegration.server_url.indexOf('127.0.0.1') !== -1)} +

    + {$t('immich.localhost_note')} +

    + {/if}