diff --git a/README.md b/README.md index 3724be9f..d6e76035 100644 --- a/README.md +++ b/README.md @@ -83,22 +83,28 @@ Enjoy AdventureLog! 🎉 # Screenshots -![Adventure Page](screenshots/adventures.png) -Displaying the adventures you have visited and the ones you plan to embark on. You can also filter and sort the adventures. +![Adventure Page](brand/screenshots/adventures.png) +Displays the adventures you have visited and the ones you plan to embark on. You can also filter and sort the adventures. -![Detail Page](screenshots/details.png) +![Detail Page](brand/screenshots/details.png) Shows specific details about an adventure, including the name, date, location, description, and rating. -![Edit](screenshots/edit.png) +![Edit](brand/screenshots/edit.png) -![Map Page](screenshots/map.png) +![Map Page](brand/screenshots/map.png) View all of your adventures on a map, with the ability to filter by visit status and add new ones by click on the map. -![Itinerary Page](screenshots/itinerary.png) +![Dashboard Page](brand/screenshots/dashboard.png) +Displays a summary of your adventures, including your world travel stats. -![Country Page](screenshots/countries.png) +![Itinerary Page](brand/screenshots/itinerary.png) +Plan your adventures and travel itinerary with a list of activities and a map view. View your trip in a variety of ways, including an itinerary list, a map view, and a calendar view. -![Region Page](screenshots/regions.png) +![Country Page](brand/screenshots/countries.png) +Lists all the countries you have visited and plan to visit, with the ability to filter by visit status. + +![Region Page](brand/screenshots/regions.png) +Displays the regions for a specific country, includes a map view to visually select regions. # About AdventureLog 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/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/adventures/migrations/0016_alter_adventureimage_image.py b/backend/server/adventures/migrations/0016_alter_adventureimage_image.py new file mode 100644 index 00000000..a226fe17 --- /dev/null +++ b/backend/server/adventures/migrations/0016_alter_adventureimage_image.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.8 on 2025-01-01 21:40 + +import adventures.models +import django_resized.forms +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0015_transportation_destination_latitude_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='adventureimage', + name='image', + field=django_resized.forms.ResizedImageField(crop=None, force_format='WEBP', keep_meta=True, quality=75, scale=None, size=[1920, 1080], upload_to=adventures.models.PathAndRename('images/')), + ), + ] 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 98ae2682..c77bc4dd 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -1,7 +1,9 @@ from collections.abc import Collection +import os from typing import Iterable import uuid from django.db import models +from django.utils.deconstruct import deconstructible from django.contrib.auth import get_user_model from django.contrib.postgres.fields import ArrayField @@ -257,12 +259,28 @@ def clean(self): def __str__(self): return self.name +@deconstructible +class PathAndRename: + def __init__(self, path): + self.path = path + + def __call__(self, instance, filename): + ext = filename.split('.')[-1] + # Generate a new UUID for the filename + filename = f"{uuid.uuid4()}.{ext}" + return os.path.join(self.path, filename) + class AdventureImage(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) user_id = models.ForeignKey( User, on_delete=models.CASCADE, default=default_user_id) - image = ResizedImageField(force_format="WEBP", quality=75, upload_to='images/') + image = ResizedImageField( + force_format="WEBP", + quality=75, + 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..2c677f73 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): @@ -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/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/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..1bf029b3 --- /dev/null +++ b/backend/server/integrations/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.8 on 2025-01-02 23:16 + +import django.db.models.deletion +import uuid +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=[ + ('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/__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..9db8a07c --- /dev/null +++ b/backend/server/integrations/models.py @@ -0,0 +1,15 @@ +from django.db import models +from django.contrib.auth import get_user_model +import uuid + +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) + id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) + + def __str__(self): + return self.user.username + ' - ' + self.server_url \ No newline at end of file diff --git a/backend/server/integrations/serializers.py b/backend/server/integrations/serializers.py new file mode 100644 index 00000000..cc92d211 --- /dev/null +++ b/backend/server/integrations/serializers.py @@ -0,0 +1,13 @@ +from .models import ImmichIntegration +from rest_framework import serializers + +class ImmichIntegrationSerializer(serializers.ModelSerializer): + class Meta: + model = ImmichIntegration + fields = '__all__' + read_only_fields = ['id', 'user'] + + def to_representation(self, instance): + representation = super().to_representation(instance) + representation.pop('user', None) + return representation 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..a15bbd0d --- /dev/null +++ b/backend/server/integrations/urls.py @@ -0,0 +1,14 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from integrations.views import ImmichIntegrationView, IntegrationView, ImmichIntegrationViewSet + +# Create the router and register the ViewSet +router = DefaultRouter() +router.register(r'immich', ImmichIntegrationView, basename='immich') +router.register(r'', IntegrationView, basename='integrations') +router.register(r'immich', ImmichIntegrationViewSet, basename='immich_viewset') + +# 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..935e28c2 --- /dev/null +++ b/backend/server/integrations/views.py @@ -0,0 +1,314 @@ +import os +from rest_framework.response import Response +from rest_framework import viewsets, status + +from .serializers import ImmichIntegrationSerializer +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 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' + 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. + 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 + ) + + # 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 + ) + + if 'assets' in res and 'items' in res['assets']: + 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( + { + 'message': 'No items found.', + 'error': True, + 'code': 'immich.no_items_found' + }, + status=status.HTTP_404_NOT_FOUND + ) + + @action(detail=False, methods=['get'], url_path='get/(?P[^/.]+)') + def get(self, request, imageid=None): + """ + RESTful GET method for retrieving a specific Immich image by ID. + """ + # 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 + ) + + @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] + serializer_class = ImmichIntegrationSerializer + queryset = ImmichIntegration.objects.all() + + def get_queryset(self): + return ImmichIntegration.objects.filter(user=self.request.user) + + def create(self, request): + """ + RESTful POST method for creating a new Immich integration. + """ + + # Check if the user already has an integration + user_integrations = ImmichIntegration.objects.filter(user=request.user) + if user_integrations.exists(): + return Response( + { + 'message': 'You already have an active Immich integration.', + 'error': True, + 'code': 'immich.integration_exists' + }, + status=status.HTTP_400_BAD_REQUEST + ) + + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + serializer.save(user=request.user) + return Response( + serializer.data, + status=status.HTTP_201_CREATED + ) + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) + + def destroy(self, request, pk=None): + """ + RESTful DELETE method for deleting an existing Immich integration. + """ + integration = ImmichIntegration.objects.filter(user=request.user, id=pk).first() + if not integration: + return Response( + { + 'message': 'Integration not found.', + 'error': True, + 'code': 'immich.integration_not_found' + }, + status=status.HTTP_404_NOT_FOUND + ) + integration.delete() + return Response( + { + 'message': 'Integration deleted successfully.' + }, + status=status.HTTP_200_OK + ) + + def list(self, request, *args, **kwargs): + # If the user has an integration, we only want to return that integration + + user_integrations = ImmichIntegration.objects.filter(user=request.user) + if user_integrations.exists(): + integration = user_integrations.first() + serializer = self.serializer_class(integration) + return Response( + serializer.data, + status=status.HTTP_200_OK + ) + else: + return Response( + { + 'message': 'No integration found.', + 'error': True, + 'code': 'immich.integration_not_found' + }, + status=status.HTTP_404_NOT_FOUND + ) \ No newline at end of file diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 7e4973b3..5acecb74 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -56,6 +56,7 @@ 'adventures', 'worldtravel', 'users', + 'integrations', 'django.contrib.gis', ) @@ -164,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' @@ -222,10 +220,16 @@ 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', } -SWAGGER_SETTINGS = { - 'LOGIN_URL': 'login', - 'LOGOUT_URL': 'logout', -} +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()] 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) 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/backend/server/worldtravel/management/commands/download-countries.py b/backend/server/worldtravel/management/commands/download-countries.py index c9dff837..06382cbb 100644 --- a/backend/server/worldtravel/management/commands/download-countries.py +++ b/backend/server/worldtravel/management/commands/download-countries.py @@ -68,6 +68,8 @@ def handle(self, *args, **options): country_name = country['name'] country_subregion = country['subregion'] country_capital = country['capital'] + longitude = round(float(country['longitude']), 6) if country['longitude'] else None + latitude = round(float(country['latitude']), 6) if country['latitude'] else None processed_country_codes.add(country_code) @@ -76,13 +78,17 @@ def handle(self, *args, **options): country_obj.name = country_name country_obj.subregion = country_subregion country_obj.capital = country_capital + country_obj.longitude = longitude + country_obj.latitude = latitude countries_to_update.append(country_obj) else: country_obj = Country( name=country_name, country_code=country_code, subregion=country_subregion, - capital=country_capital + capital=country_capital, + longitude=longitude, + latitude=latitude ) countries_to_create.append(country_obj) diff --git a/backend/server/worldtravel/migrations/0011_country_latitude_country_longitude.py b/backend/server/worldtravel/migrations/0011_country_latitude_country_longitude.py new file mode 100644 index 00000000..8916896e --- /dev/null +++ b/backend/server/worldtravel/migrations/0011_country_latitude_country_longitude.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.8 on 2025-01-02 00:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0010_country_capital'), + ] + + operations = [ + migrations.AddField( + model_name='country', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='country', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + ] diff --git a/backend/server/worldtravel/models.py b/backend/server/worldtravel/models.py index f7f8f996..2acc629c 100644 --- a/backend/server/worldtravel/models.py +++ b/backend/server/worldtravel/models.py @@ -15,6 +15,8 @@ class Country(models.Model): country_code = models.CharField(max_length=2, unique=True) #iso2 code subregion = models.CharField(max_length=100, blank=True, null=True) capital = models.CharField(max_length=100, blank=True, null=True) + longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) class Meta: verbose_name = "Country" diff --git a/backend/server/worldtravel/serializers.py b/backend/server/worldtravel/serializers.py index 0f2ed734..70f569bb 100644 --- a/backend/server/worldtravel/serializers.py +++ b/backend/server/worldtravel/serializers.py @@ -29,7 +29,7 @@ def get_num_visits(self, obj): class Meta: model = Country fields = '__all__' - read_only_fields = ['id', 'name', 'country_code', 'subregion', 'flag_url', 'num_regions', 'num_visits'] + read_only_fields = ['id', 'name', 'country_code', 'subregion', 'flag_url', 'num_regions', 'num_visits', 'longitude', 'latitude', 'capital'] class RegionSerializer(serializers.ModelSerializer): diff --git a/brand/adventurelog.png b/brand/adventurelog.png new file mode 100644 index 00000000..21325e54 Binary files /dev/null and b/brand/adventurelog.png differ diff --git a/brand/adventurelog.svg b/brand/adventurelog.svg new file mode 100644 index 00000000..92667f25 --- /dev/null +++ b/brand/adventurelog.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/brand/banner.png b/brand/banner.png new file mode 100644 index 00000000..a0dd0ae7 Binary files /dev/null and b/brand/banner.png differ diff --git a/brand/screenshots/adventures.png b/brand/screenshots/adventures.png new file mode 100644 index 00000000..fd61d549 Binary files /dev/null and b/brand/screenshots/adventures.png differ diff --git a/brand/screenshots/countries.png b/brand/screenshots/countries.png new file mode 100644 index 00000000..3b8e5346 Binary files /dev/null and b/brand/screenshots/countries.png differ diff --git a/brand/screenshots/dashboard.png b/brand/screenshots/dashboard.png new file mode 100644 index 00000000..af4d8bb6 Binary files /dev/null and b/brand/screenshots/dashboard.png differ diff --git a/brand/screenshots/details.png b/brand/screenshots/details.png new file mode 100644 index 00000000..6ae57eb9 Binary files /dev/null and b/brand/screenshots/details.png differ diff --git a/brand/screenshots/edit.png b/brand/screenshots/edit.png new file mode 100644 index 00000000..123160d7 Binary files /dev/null and b/brand/screenshots/edit.png differ diff --git a/brand/screenshots/itinerary.png b/brand/screenshots/itinerary.png new file mode 100644 index 00000000..f1532637 Binary files /dev/null and b/brand/screenshots/itinerary.png differ diff --git a/brand/screenshots/map.png b/brand/screenshots/map.png new file mode 100644 index 00000000..22b13b9d Binary files /dev/null and b/brand/screenshots/map.png differ diff --git a/brand/screenshots/regions.png b/brand/screenshots/regions.png new file mode 100644 index 00000000..6092dc63 Binary files /dev/null and b/brand/screenshots/regions.png differ diff --git a/documentation/.vitepress/config.mts b/documentation/.vitepress/config.mts index 1c47f293..4f1264c6 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", @@ -131,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/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` diff --git a/documentation/docs/configuration/immich_integration.md b/documentation/docs/configuration/immich_integration.md new file mode 100644 index 00000000..a8b2ae15 --- /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`. 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. + +Enjoy the privacy and control of managing your travel media with Immich and AdventureLog! 🎉 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/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/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index 0d903eea..b77b8eaa 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -191,7 +191,7 @@ {#if type != 'link'} - {#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))} + {#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))} {: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 immichIntegration} + { + url = e.detail; + fetchImage(); + }} + /> + {/if} + +
    + + {#if images.length > 0} +

    {$t('adventures.my_images')}

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

    {$t('adventures.no_images')}

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

    {$t('adventures.share_adventure')}

    diff --git a/frontend/src/lib/components/CardCarousel.svelte b/frontend/src/lib/components/CardCarousel.svelte index 07718bd4..edbf4697 100644 --- a/frontend/src/lib/components/CardCarousel.svelte +++ b/frontend/src/lib/components/CardCarousel.svelte @@ -9,7 +9,11 @@ let image_url: string | null = null; $: adventure_images = adventures.flatMap((adventure) => - 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/components/ImmichSelect.svelte b/frontend/src/lib/components/ImmichSelect.svelte new file mode 100644 index 00000000..8668f6ff --- /dev/null +++ b/frontend/src/lib/components/ImmichSelect.svelte @@ -0,0 +1,175 @@ + + +
    + +
    +
    + (currentAlbum = '')} + type="radio" + class="join-item btn" + bind:group={searchOrSelect} + value="search" + aria-label="Search" + /> + +
    +
    + {#if searchOrSelect === 'search'} +
    + + +
    + {:else} + + {/if} +
    +
    + +

    {immichError}

    +
    + {#each immichImages as image} +
    + + Image from Immich + +
    + {/each} + {#if immichNext} + + {/if} +
    +
    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/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/lib/types.ts b/frontend/src/lib/types.ts index 9dac4621..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; @@ -50,6 +51,8 @@ export type Country = { capital: string; num_regions: number; num_visits: number; + longitude: number | null; + latitude: number | null; }; export type Region = { @@ -194,3 +197,37 @@ export type Category = { user_id: string; num_adventures?: number | null; }; + +export type ImmichIntegration = { + id: string; + 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; +}; diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index c2485cac..dfb35ccb 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -215,7 +215,9 @@ "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", + "will_be_marked": "wird als besucht markiert, sobald das Abenteuer gespeichert ist." }, "home": { "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", @@ -454,17 +456,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", @@ -500,5 +492,30 @@ "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", + "documentation": "Immich-Integrationsdokumentation", + "localhost_note": "Hinweis: localhost wird höchstwahrscheinlich nicht funktionieren, es sei denn, Sie haben Docker-Netzwerke entsprechend eingerichtet. \nEs wird empfohlen, die IP-Adresse des Servers oder den Domänennamen zu verwenden." } } diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index d8478266..a7e668e8 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -229,8 +229,10 @@ "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", "emoji_picker": "Emoji Picker", "download_calendar": "Download Calendar", "date_information": "Date Information", @@ -466,17 +468,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", @@ -500,5 +492,30 @@ "recent_adventures": "Recent Adventures", "no_recent_adventures": "No recent adventures?", "add_some": "Why not start planning your next adventure? You can add a new adventure by clicking the button below." + }, + "immich": { + "immich": "Immich", + "integration_fetch_error": "Error fetching data from the Immich integration", + "integration_missing": "The Immich integration is missing from the backend", + "query_required": "Query is required", + "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", + "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", + "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/locales/es.json b/frontend/src/locales/es.json index 1164d25e..0211650a 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -262,7 +262,9 @@ "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", + "will_be_marked": "se marcará como visitado una vez guardada la aventura." }, "worldtravel": { "all": "Todo", @@ -466,17 +468,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", @@ -500,5 +492,30 @@ "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", + "documentation": "Documentación de integración de Immich", + "localhost_note": "Nota: lo más probable es que localhost no funcione a menos que haya configurado las redes acoplables en consecuencia. \nSe recomienda utilizar la dirección IP del servidor o el nombre de dominio." } } diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index c66bf036..45726194 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -215,7 +215,9 @@ "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", + "will_be_marked": "sera marqué comme visité une fois l’aventure sauvegardée." }, "home": { "desc_1": "Découvrez, planifiez et explorez en toute simplicité", @@ -454,17 +456,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", @@ -500,5 +492,30 @@ "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", + "documentation": "Documentation d'intégration Immich", + "localhost_note": "Remarque : localhost ne fonctionnera probablement pas à moins que vous n'ayez configuré les réseaux Docker en conséquence. \nIl est recommandé d'utiliser l'adresse IP du serveur ou le nom de domaine." } } diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 21bee779..9c06a229 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -215,7 +215,9 @@ "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", + "will_be_marked": "verrà contrassegnato come visitato una volta salvata l'avventura." }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", @@ -454,17 +456,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", @@ -500,5 +492,30 @@ "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", + "documentation": "Documentazione sull'integrazione di Immich", + "localhost_note": "Nota: molto probabilmente localhost non funzionerà a meno che tu non abbia configurato le reti docker di conseguenza. \nSi consiglia di utilizzare l'indirizzo IP del server o il nome del dominio." } } diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index c2a59a57..91fb42bc 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -215,7 +215,9 @@ "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", + "will_be_marked": "wordt gemarkeerd als bezocht zodra het avontuur is opgeslagen." }, "home": { "desc_1": "Ontdek, plan en verken met gemak", @@ -454,17 +456,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", @@ -500,5 +492,30 @@ "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", + "documentation": "Immich-integratiedocumentatie", + "localhost_note": "Opmerking: localhost zal hoogstwaarschijnlijk niet werken tenzij u de docker-netwerken dienovereenkomstig hebt ingesteld. \nHet wordt aanbevolen om het IP-adres van de server of de domeinnaam te gebruiken." } } diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index eac96345..641aa955 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -262,7 +262,9 @@ "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ę", + "will_be_marked": "zostanie oznaczona jako odwiedzona po zapisaniu przygody." }, "worldtravel": { "country_list": "Lista krajów", @@ -466,17 +468,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", @@ -500,5 +492,30 @@ "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!", + "documentation": "Dokumentacja integracji Immicha", + "localhost_note": "Uwaga: localhost najprawdopodobniej nie będzie działać, jeśli nie skonfigurujesz odpowiednio sieci dokerów. \nZalecane jest użycie adresu IP serwera lub nazwy domeny." } } diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index 7beccea6..505fbb6b 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,9 @@ "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", + "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", @@ -455,17 +456,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", @@ -501,5 +492,30 @@ "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", + "documentation": "Immich Integrationsdokumentation", + "localhost_note": "Obs: localhost kommer sannolikt inte att fungera om du inte har konfigurerat docker-nätverk i enlighet med detta. \nDet rekommenderas att använda serverns IP-adress eller domännamnet." } } diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 0a686a2a..86674c52 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -215,7 +215,9 @@ "start": "开始", "starting_airport": "出发机场", "to": "到", - "transportation_delete_confirm": "您确定要删除此交通工具吗?\n此操作无法撤消。" + "transportation_delete_confirm": "您确定要删除此交通工具吗?\n此操作无法撤消。", + "show_map": "显示地图", + "will_be_marked": "保存冒险后将被标记为已访问。" }, "home": { "desc_1": "轻松发现、规划和探索", @@ -454,17 +456,7 @@ "show_visited_regions": "显示访问过的地区", "view_details": "查看详情" }, - "languages": { - "de": "德语", - "en": "英语", - "es": "西班牙语", - "fr": "法语", - "it": "意大利语", - "nl": "荷兰语", - "sv": "瑞典", - "zh": "中国人", - "pl": "波兰语" - }, + "languages": {}, "share": { "no_users_shared": "没有与之共享的用户", "not_shared_with": "不与共享", @@ -500,5 +492,30 @@ "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": "更新集成", + "documentation": "Immich 集成文档", + "localhost_note": "注意:除非您相应地设置了 docker 网络,否则 localhost 很可能无法工作。\n建议使用服务器的IP地址或域名。" } } 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; } diff --git a/frontend/src/routes/immich/[key]/+server.ts b/frontend/src/routes/immich/[key]/+server.ts new file mode 100644 index 00000000..33d33de8 --- /dev/null +++ b/frontend/src/routes/immich/[key]/+server.ts @@ -0,0 +1,54 @@ +import type { RequestHandler } from './$types'; + +const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + +export const GET: RequestHandler = async (event) => { + try { + const key = event.params.key; + + // Forward the session ID from cookies + const sessionid = event.cookies.get('sessionid'); + if (!sessionid) { + return new Response(JSON.stringify({ error: 'Session ID is missing' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } + + // Proxy the request to the backend + const res = await fetch(`${endpoint}/api/integrations/immich/get/${key}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Cookie: `sessionid=${sessionid}` + } + }); + + if (!res.ok) { + // Return an error response if the backend request fails + const errorData = await res.json(); + return new Response(JSON.stringify(errorData), { + status: res.status, + headers: { 'Content-Type': 'application/json' } + }); + } + + // Get the image as a Blob + const image = await res.blob(); + + // Create a Response to pass the image back + return new Response(image, { + status: res.status, + headers: { + 'Content-Type': res.headers.get('Content-Type') || 'image/jpeg' + } + }); + } catch (error) { + console.error('Error proxying request:', error); + return new Response(JSON.stringify({ error: 'Failed to fetch image' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +}; diff --git a/frontend/src/routes/settings/+page.server.ts b/frontend/src/routes/settings/+page.server.ts index beb84327..ca29ee61 100644 --- a/frontend/src/routes/settings/+page.server.ts +++ b/frontend/src/routes/settings/+page.server.ts @@ -1,7 +1,7 @@ import { fail, redirect, type Actions } from '@sveltejs/kit'; import type { PageServerLoad } from '../$types'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; -import type { User } from '$lib/types'; +import type { ImmichIntegration, User } from '$lib/types'; import { fetchCSRFToken } from '$lib/index.server'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -56,11 +56,22 @@ export const load: PageServerLoad = async (event) => { let mfaAuthenticatorResponse = (await mfaAuthenticatorFetch.json()) as MFAAuthenticatorResponse; let authenticators = (mfaAuthenticatorResponse.data.length > 0) as boolean; + let immichIntegration: ImmichIntegration | null = null; + let immichIntegrationsFetch = await fetch(`${endpoint}/api/integrations/immich/`, { + headers: { + Cookie: `sessionid=${sessionId}` + } + }); + if (immichIntegrationsFetch.ok) { + immichIntegration = await immichIntegrationsFetch.json(); + } + return { props: { user, emails, - authenticators + authenticators, + immichIntegration } }; }; diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 0c0b10ab..6c2b97f9 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -2,14 +2,16 @@ import { enhance } from '$app/forms'; import { page } from '$app/stores'; import { addToast } from '$lib/toasts'; - import type { User } from '$lib/types.js'; + import type { ImmichIntegration, User } from '$lib/types.js'; import { onMount } from 'svelte'; import { browser } from '$app/environment'; import { t } from 'svelte-i18n'; import TotpModal from '$lib/components/TOTPModal.svelte'; import { appTitle, appVersion } from '$lib/config.js'; + import ImmichLogo from '$lib/assets/immich.svg'; export let data; + console.log(data); let user: User; let emails: typeof data.props.emails; if (data.user) { @@ -19,6 +21,14 @@ let new_email: string = ''; + let immichIntegration = data.props.immichIntegration; + + let newImmichIntegration: ImmichIntegration = { + server_url: '', + api_key: '', + id: '' + }; + let isMFAModalOpen: boolean = false; onMount(async () => { @@ -131,6 +141,54 @@ } } + async function enableImmichIntegration() { + if (!immichIntegration?.id) { + let res = await fetch('/api/integrations/immich/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(newImmichIntegration) + }); + let data = await res.json(); + if (res.ok) { + addToast('success', $t('immich.immich_enabled')); + immichIntegration = data; + } else { + addToast('error', $t('immich.immich_error')); + } + } else { + let res = await fetch(`/api/integrations/immich/${immichIntegration.id}/`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(newImmichIntegration) + }); + let data = await res.json(); + if (res.ok) { + addToast('success', $t('immich.immich_updated')); + immichIntegration = data; + } else { + addToast('error', $t('immich.immich_error')); + } + } + } + + async function disableImmichIntegration() { + if (immichIntegration && immichIntegration.id) { + let res = await fetch(`/api/integrations/immich/${immichIntegration.id}/`, { + method: 'DELETE' + }); + if (res.ok) { + addToast('success', $t('immich.immich_disabled')); + immichIntegration = null; + } else { + addToast('error', $t('immich.immich_error')); + } + } + } + async function disableMfa() { const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', { method: 'DELETE' @@ -174,7 +232,9 @@ class="space-y-6" >
    - +
    - +
    - +
    - + - +
    @@ -240,7 +308,7 @@
    -
    - +
    - {/each} {#if emails.length === 0} -

    {$t('settings.no_email_set')}

    +

    {$t('settings.no_email_set')}

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

    {$t('settings.mfa_not_enabled')}

    +

    {$t('settings.mfa_not_enabled')}

    @@ -354,6 +424,85 @@
    + +
    +

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

    +
    +

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

    + {#if immichIntegration} +
    +
    {$t('immich.integration_enabled')}
    +
    + + +
    +
    + {/if} + {#if !immichIntegration || newImmichIntegration.id} +
    +
    + + + {#if newImmichIntegration.server_url && !newImmichIntegration.server_url.endsWith('api')} +

    + {$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} +
    +
    + + +
    + +
    + {/if} +
    +
    +

    {$t('adventures.visited_region_check')}

    diff --git a/frontend/src/routes/worldtravel/+page.svelte b/frontend/src/routes/worldtravel/+page.svelte index d27a676e..dad43a1a 100644 --- a/frontend/src/routes/worldtravel/+page.svelte +++ b/frontend/src/routes/worldtravel/+page.svelte @@ -1,8 +1,10 @@