diff --git a/LICENSE b/LICENSE index e13fb3a..792a64f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ AdventureLog: Self-hostable travel tracker and trip planner. - Copyright (C) 2024 Sean Morley + Copyright (C) 2023-2025 Sean Morley Contact: contact@seanmorley.com This program is free software: you can redistribute it and/or modify diff --git a/README.md b/README.md index 3724be9..d6e7603 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/entrypoint.sh b/backend/entrypoint.sh index fa81d11..48adee3 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -39,4 +39,4 @@ python manage.py download-countries cat /code/adventurelog.txt # Start gunicorn -gunicorn main.wsgi:application --bind 0.0.0.0:8000 \ No newline at end of file +gunicorn main.wsgi:application --bind 0.0.0.0:8000 --timeout 120 --workers 2 \ No newline at end of file diff --git a/backend/server/.env.example b/backend/server/.env.example index 04eb77f..4c1f9ad 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 1beac0f..be1793b 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -8,8 +8,6 @@ from allauth.account.decorators import secure_admin_login 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/0015_transportation_destination_latitude_and_more.py b/backend/server/adventures/migrations/0015_transportation_destination_latitude_and_more.py new file mode 100644 index 0000000..7971839 --- /dev/null +++ b/backend/server/adventures/migrations/0015_transportation_destination_latitude_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.8 on 2024-12-19 17:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adventures', '0014_alter_category_unique_together'), + ] + + operations = [ + migrations.AddField( + model_name='transportation', + name='destination_latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='transportation', + name='destination_longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='transportation', + name='origin_latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + migrations.AddField( + model_name='transportation', + name='origin_longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + ] 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 0000000..a226fe1 --- /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 0000000..9a920a3 --- /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 a8460d7..c77bc4d 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 @@ -167,6 +169,10 @@ class Transportation(models.Model): end_date = models.DateTimeField(blank=True, null=True) flight_number = models.CharField(max_length=100, blank=True, null=True) from_location = models.CharField(max_length=200, blank=True, null=True) + origin_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + origin_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + destination_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + destination_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) to_location = models.CharField(max_length=200, blank=True, null=True) is_public = models.BooleanField(default=False) collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True) @@ -253,12 +259,28 @@ class ChecklistItem(models.Model): 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 9b538ed..2c677f7 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -8,7 +8,7 @@ from main.utils import CustomModelSerializer 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 @@ class AdventureSerializer(CustomModelSerializer): 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 @@ class AdventureSerializer(CustomModelSerializer): 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 @@ class AdventureSerializer(CustomModelSerializer): 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 @@ -170,7 +172,7 @@ class TransportationSerializer(CustomModelSerializer): fields = [ 'id', 'user_id', 'type', 'name', 'description', 'rating', 'link', 'date', 'flight_number', 'from_location', 'to_location', - 'is_public', 'collection', 'created_at', 'updated_at', 'end_date' + 'is_public', 'collection', 'created_at', 'updated_at', 'end_date', 'origin_latitude', 'origin_longitude', 'destination_latitude', 'destination_longitude' ] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index f9d9aca..3ee3e79 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -1032,7 +1032,29 @@ class AdventureImageViewSet(viewsets.ModelViewSet): @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: @@ -1233,10 +1255,24 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet): for adventure in serializer.data: if adventure['visits']: for visit in adventure['visits']: + # Skip if start_date is missing + if not visit.get('start_date'): + continue + + # Parse start_date and handle end_date + try: + start_date = datetime.strptime(visit['start_date'], '%Y-%m-%d').date() + except ValueError: + continue # Skip if the start_date is invalid + + end_date = ( + datetime.strptime(visit['end_date'], '%Y-%m-%d').date() + timedelta(days=1) + if visit.get('end_date') else start_date + timedelta(days=1) + ) + + # Create event event = Event() event.add('summary', adventure['name']) - start_date = datetime.strptime(visit['start_date'], '%Y-%m-%d').date() - end_date = datetime.strptime(visit['end_date'], '%Y-%m-%d').date() + timedelta(days=1) if visit['end_date'] else start_date + timedelta(days=1) event.add('dtstart', start_date) event.add('dtend', end_date) event.add('dtstamp', datetime.now()) diff --git a/backend/server/integrations/__init__.py b/backend/server/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/server/integrations/admin.py b/backend/server/integrations/admin.py new file mode 100644 index 0000000..d561cf4 --- /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 0000000..73adb7a --- /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 0000000..1bf029b --- /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 0000000..e69de29 diff --git a/backend/server/integrations/models.py b/backend/server/integrations/models.py new file mode 100644 index 0000000..9db8a07 --- /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 0000000..cc92d21 --- /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 0000000..7ce503c --- /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 0000000..a15bbd0 --- /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 0000000..935e28c --- /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 7e4973b..5acecb7 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -56,6 +56,7 @@ INSTALLED_APPS = ( 'adventures', 'worldtravel', 'users', + 'integrations', 'django.contrib.gis', ) @@ -164,9 +165,6 @@ TEMPLATES = [ 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 @@ REST_FRAMEWORK = { '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 3e3c53f..ab1e084 100644 --- a/backend/server/main/urls.py +++ b/backend/server/main/urls.py @@ -39,6 +39,8 @@ urlpatterns = [ # 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 be712b7..9e1d48c 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 c9dff83..06382cb 100644 --- a/backend/server/worldtravel/management/commands/download-countries.py +++ b/backend/server/worldtravel/management/commands/download-countries.py @@ -68,6 +68,8 @@ class Command(BaseCommand): 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 @@ class Command(BaseCommand): 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 0000000..8916896 --- /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 f7f8f99..2acc629 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 0f2ed73..70f569b 100644 --- a/backend/server/worldtravel/serializers.py +++ b/backend/server/worldtravel/serializers.py @@ -29,7 +29,7 @@ class CountrySerializer(serializers.ModelSerializer): 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 0000000..21325e5 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 0000000..92667f2 --- /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 0000000..a0dd0ae 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 0000000..fd61d54 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 0000000..3b8e534 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 0000000..af4d8bb 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 0000000..6ae57eb 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 0000000..123160d 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 0000000..f153263 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 0000000..22b13b9 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 0000000..6092dc6 Binary files /dev/null and b/brand/screenshots/regions.png differ diff --git a/docker-compose-traefik.yaml b/docker-compose-traefik.yaml index c6ebcf7..6fb2f03 100644 --- a/docker-compose-traefik.yaml +++ b/docker-compose-traefik.yaml @@ -39,7 +39,7 @@ services: labels: - "traefik.enable=true" - "traefik.http.routers.adventurelogweb.entrypoints=websecure" - - "traefik.http.routers.adventurelogweb.rule=Host(`yourdomain.com`) && !PathPrefix(`/media`)" # Replace with your domain + - "traefik.http.routers.adventurelogweb.rule=Host(`yourdomain.com`) && !(PathPrefix(`/media`) || PathPrefix(`/admin`) || PathPrefix(`/static`))" # Replace with your domain - "traefik.http.routers.adventurelogweb.tls=true" - "traefik.http.routers.adventurelogweb.tls.certresolver=letsencrypt" @@ -64,7 +64,7 @@ services: labels: - "traefik.enable=true" - "traefik.http.routers.adventurelogserver.entrypoints=websecure" - - "traefik.http.routers.adventurelogserver.rule=Host(`yourdomain.com`) && PathPrefix(`/media`)" # Replace with your domain + - "traefik.http.routers.adventurelogserver.rule=Host(`yourdomain.com`) && && (PathPrefix(`/media`) || PathPrefix(`/admin`) || PathPrefix(`/static`))" # Replace with your domain - "traefik.http.routers.adventurelogserver.tls=true" - "traefik.http.routers.adventurelogserver.tls.certresolver=letsencrypt" diff --git a/documentation/.vitepress/config.mts b/documentation/.vitepress/config.mts index c14b50c..4f1264c 100644 --- a/documentation/.vitepress/config.mts +++ b/documentation/.vitepress/config.mts @@ -41,7 +41,7 @@ export default defineConfig({ footer: { message: "AdventureLog", - copyright: "Copyright © 2023-2024 Sean Morley", + copyright: "Copyright © 2023-2025 Sean Morley", }, logo: "/adventurelog.png", @@ -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 0000000..48d9c18 --- /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 f3fb313..5312910 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 0000000..a8b2ae1 --- /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/documentation/pnpm-lock.yaml b/documentation/pnpm-lock.yaml index 0371b16..109ad48 100644 --- a/documentation/pnpm-lock.yaml +++ b/documentation/pnpm-lock.yaml @@ -612,8 +612,8 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1352,7 +1352,7 @@ snapshots: mitt@3.0.1: {} - nanoid@3.3.7: {} + nanoid@3.3.8: {} oniguruma-to-es@0.4.1: dependencies: @@ -1366,7 +1366,7 @@ snapshots: postcss@8.4.49: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.1.1 source-map-js: 1.2.1 diff --git a/frontend/package.json b/frontend/package.json index da9ceec..442a9d6 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", @@ -41,6 +41,7 @@ "dependencies": { "@lukulent/svelte-umami": "^0.0.3", "emoji-picker-element": "^1.26.0", + "marked": "^15.0.4", "qrcode": "^1.5.4", "svelte-i18n": "^4.0.1", "svelte-maplibre": "^0.9.8" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 702ef84..1c141b3 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: emoji-picker-element: specifier: ^1.26.0 version: 1.26.0 + marked: + specifier: ^15.0.4 + version: 15.0.4 qrcode: specifier: ^1.5.4 version: 1.5.4 @@ -899,8 +902,8 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} css-selector-tokenizer@0.8.0: @@ -1351,6 +1354,11 @@ packages: resolution: {integrity: sha512-qOS1hn4d/pn2i0uva4S5Oz+fACzTkgBKq+NpwT/Tqzi4MSyzcWNtDELzLUSgWqHfNIkGCl5CZ/w7dtis+t4RCw==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} + marked@15.0.4: + resolution: {integrity: sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw==} + engines: {node: '>= 18'} + hasBin: true + mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} @@ -1432,8 +1440,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2358,7 +2366,7 @@ snapshots: '@jsdevtools/ez-spawn@3.0.4': dependencies: call-me-maybe: 1.0.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 string-argv: 0.3.2 type-detect: 4.0.8 @@ -2819,7 +2827,7 @@ snapshots: cookie@0.6.0: {} - cross-spawn@7.0.3: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -3000,7 +3008,7 @@ snapshots: execa@5.1.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 6.0.1 human-signals: 2.1.0 is-stream: 2.0.1 @@ -3057,7 +3065,7 @@ snapshots: foreground-child@3.2.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 fraction.js@4.3.7: {} @@ -3318,6 +3326,8 @@ snapshots: tinyqueue: 2.0.3 vt-pbf: 3.1.3 + marked@15.0.4: {} + mdn-data@2.0.30: {} memoizee@0.4.17: @@ -3394,7 +3404,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.7: {} + nanoid@3.3.8: {} next-tick@1.1.0: {} @@ -3548,13 +3558,13 @@ snapshots: postcss@8.4.38: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.0.1 source-map-js: 1.2.0 postcss@8.4.47: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.1.0 source-map-js: 1.2.1 diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 98830a8..91e1b60 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -23,7 +23,7 @@ export const authHook: Handle = async ({ event, resolve }) => { if (!userFetch.ok) { event.locals.user = null; - event.cookies.delete('sessionid', { path: '/' }); + event.cookies.delete('sessionid', { path: '/', secure: event.url.protocol === 'https:' }); return await resolve(event); } @@ -47,19 +47,19 @@ export const authHook: Handle = async ({ event, resolve }) => { path: '/', httpOnly: true, sameSite: 'lax', - secure: true, + secure: event.url.protocol === 'https:', expires: expiryDate }); } } } else { event.locals.user = null; - event.cookies.delete('sessionid', { path: '/' }); + event.cookies.delete('sessionid', { path: '/', secure: event.url.protocol === 'https:' }); } } catch (error) { console.error('Error in authHook:', error); event.locals.user = null; - event.cookies.delete('sessionid', { path: '/' }); + event.cookies.delete('sessionid', { path: '/', secure: event.url.protocol === 'https:' }); } return await resolve(event); diff --git a/frontend/src/lib/assets/immich.svg b/frontend/src/lib/assets/immich.svg new file mode 100644 index 0000000..70aa672 --- /dev/null +++ b/frontend/src/lib/assets/immich.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/lib/components/AboutModal.svelte b/frontend/src/lib/components/AboutModal.svelte index 60faa89..7b8fbf2 100644 --- a/frontend/src/lib/components/AboutModal.svelte +++ b/frontend/src/lib/components/AboutModal.svelte @@ -1,13 +1,14 @@ - - + - - + +
    +

    + © {copyrightYear} + + Sean Morley + +

    +

    {$t('about.license')}

    +

    + + {$t('about.source_code')} + +

    +

    {$t('about.message')}

    +
    + + +
    + + +
    +

    + {$t('about.oss_attributions')} +

    +

    + {$t('about.nominatim_1')} + + OpenStreetMap + + . {$t('about.nominatim_2')} +

    +

    {$t('about.other_attributions')}

    +
    + + +
    + +
    + + diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte index da80e43..b77b8ea 100644 --- a/frontend/src/lib/components/AdventureCard.svelte +++ b/frontend/src/lib/components/AdventureCard.svelte @@ -41,6 +41,24 @@ } } + let unlinked: boolean = false; + + // Reactive block to update `unlinked` when dependencies change + $: { + if (collection && collection?.start_date && collection.end_date) { + unlinked = adventure.visits.every((visit) => { + // Check if visit dates exist + if (!visit.start_date || !visit.end_date) return true; // Consider "unlinked" for incomplete visit data + + // Check if collection dates are completely outside this visit's range + const isBeforeVisit = collection.end_date && collection.end_date < visit.start_date; + const isAfterVisit = collection.start_date && collection.start_date > visit.end_date; + + return isBeforeVisit || isAfterVisit; + }); + } + } + async function deleteAdventure() { let res = await fetch(`/adventures/${adventure.id}?/delete`, { method: 'POST', @@ -140,6 +158,9 @@ {adventure.is_public ? $t('adventures.public') : $t('adventures.private')} + {#if unlinked} +
    {$t('adventures.out_of_range')}
    + {/if} {#if adventure.location && adventure.location !== ''}
    @@ -170,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))}
    diff --git a/frontend/src/lib/components/PointSelectionModal.svelte b/frontend/src/lib/components/PointSelectionModal.svelte index f7f1333..56f734f 100644 --- a/frontend/src/lib/components/PointSelectionModal.svelte +++ b/frontend/src/lib/components/PointSelectionModal.svelte @@ -111,7 +111,7 @@

    Choose a Point

    +
    +

    {transportation.name}

    +
    +
    + {$t(`transportation.modes.${transportation.type}`)} +
    + {#if transportation.type == 'plane' && transportation.flight_number} +
    {transportation.flight_number}
    + {/if} +
    -
    - {#if transportation.date} -

    {new Date(transportation.date).toLocaleString(undefined, { timeZone: 'UTC' })}

    + {#if unlinked} +
    {$t('adventures.out_of_range')}
    + {/if} + + +
    + {#if transportation.from_location} +
    + {$t('adventures.from')}: +

    {transportation.from_location}

    +
    {/if} - {#if transportation.end_date} - -

    {new Date(transportation.end_date).toLocaleString(undefined, { timeZone: 'UTC' })}

    + {#if transportation.date} +
    + {$t('adventures.start')}: +

    {new Date(transportation.date).toLocaleString(undefined, { timeZone: 'UTC' })}

    +
    {/if}
    - {#if transportation.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))} + +
    + {#if transportation.to_location} + +
    + {$t('adventures.to')}: + +

    {transportation.to_location}

    +
    + {/if} + {#if transportation.end_date} +
    + {$t('adventures.end')}: +

    {new Date(transportation.end_date).toLocaleString(undefined, { timeZone: 'UTC' })}

    +
    + {/if} +
    + + + {#if transportation.user_id == user?.uuid || (collection && user && collection.shared_with && collection.shared_with.includes(user.uuid))}
    - - +
    {/if} diff --git a/frontend/src/lib/components/TransportationModal.svelte b/frontend/src/lib/components/TransportationModal.svelte new file mode 100644 index 0000000..9258b10 --- /dev/null +++ b/frontend/src/lib/components/TransportationModal.svelte @@ -0,0 +1,605 @@ + + + + + + + diff --git a/frontend/src/lib/components/UserCard.svelte b/frontend/src/lib/components/UserCard.svelte index 3deb52d..aefa17e 100644 --- a/frontend/src/lib/components/UserCard.svelte +++ b/frontend/src/lib/components/UserCard.svelte @@ -17,34 +17,46 @@ class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl" >
    -
    + +
    {#if user.profile_pic} -
    -
    +
    +
    {user.username}
    {/if} -

    {user.first_name} {user.last_name}

    + +

    + {user.first_name} + {user.last_name} +

    +

    {user.username}

    + + + {#if user.is_staff} +
    Admin
    + {/if}
    -

    {user.username}

    - {#if user.is_staff} -
    Admin
    - {/if} - -
    - -

    + + +

    + +

    {user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''}

    -
    + + +
    {#if !sharing} - + {:else if shared_with && !shared_with.includes(user.uuid)} - + {:else} - + {/if}
    diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index 15ee7ef..d201582 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -1,4 +1,4 @@ -export let appVersion = 'Web 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 = '2024'; +export let copyrightYear = '2023-2025'; diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index 1d1a3bd..b476fe0 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -289,6 +289,37 @@ export function getAdventureTypeLabel(type: string) { } export function getRandomBackground() { + const today = new Date(); + + // Special dates for specific backgrounds + // New Years week + + const newYearsStart = new Date(today.getFullYear() - 1, 11, 31); + newYearsStart.setHours(0, 0, 0, 0); + const newYearsEnd = new Date(today.getFullYear(), 0, 2); + newYearsEnd.setHours(23, 59, 59, 999); + if (today >= newYearsStart && today <= newYearsEnd) { + return { + url: 'backgrounds/adventurelog_new_year.webp', + author: 'Roven Images', + location: "Happy New Year's from the AdventureLog team!" + } as Background; + } + + // Christmas 12/24 - 12/25 + const christmasStart = new Date(today.getFullYear(), 11, 24); + christmasStart.setHours(0, 0, 0, 0); + const christmasEnd = new Date(today.getFullYear(), 11, 25); + christmasEnd.setHours(23, 59, 59, 999); + + if (today >= christmasStart && today <= christmasEnd) { + return { + url: 'backgrounds/adventurelog_christmas.webp', + author: 'Annie Spratt', + location: 'Merry Christmas from the AdventureLog team!' + } as Background; + } + const randomIndex = Math.floor(Math.random() * randomBackgrounds.backgrounds.length); return randomBackgrounds.backgrounds[randomIndex] as Background; } diff --git a/frontend/src/lib/json/backgrounds.json b/frontend/src/lib/json/backgrounds.json index d1b60c5..be43594 100644 --- a/frontend/src/lib/json/backgrounds.json +++ b/frontend/src/lib/json/backgrounds.json @@ -19,6 +19,11 @@ "url": "backgrounds/adventurelog_showcase_4.webp", "author": "Sean Morley", "location": "Great Sand Dunes National Park, Colorado, USA" + }, + { + "url": "backgrounds/adventurelog_showcase_5.webp", + "author": "Sean Morley", + "location": "Hoboken, New Jersey, USA" } ] } diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 181017b..ea9e1fb 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 = { @@ -86,14 +89,14 @@ export type Collection = { description: string; is_public: boolean; adventures: Adventure[]; - created_at?: string; - start_date?: string; - end_date?: string; + created_at?: string | null; + start_date: string | null; + end_date: string | null; transportations?: Transportation[]; notes?: Note[]; checklists?: Checklist[]; is_archived?: boolean; - shared_with: string[]; + shared_with: string[] | undefined; link?: string | null; }; @@ -127,8 +130,12 @@ export type Transportation = { flight_number: string | null; from_location: string | null; to_location: string | null; + origin_latitude: number | null; + origin_longitude: number | null; + destination_latitude: number | null; + destination_longitude: number | null; is_public: boolean; - collection: Collection | null; + collection: Collection | null | string; created_at: string; // ISO 8601 date string updated_at: string; // ISO 8601 date string }; @@ -190,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 25e2b41..dfb35cc 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -195,7 +195,29 @@ "emoji_picker": "Emoji-Picker", "hide": "Verstecken", "show": "Zeigen", - "download_calendar": "Kalender herunterladen" + "download_calendar": "Kalender herunterladen", + "md_instructions": "Schreiben Sie hier Ihren Abschlag...", + "preview": "Vorschau", + "checklist_delete_confirm": "Sind Sie sicher, dass Sie diese Checkliste löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.", + "clear_location": "Standort löschen", + "date_information": "Datumsinformationen", + "delete_checklist": "Checkliste löschen", + "delete_note": "Notiz löschen", + "delete_transportation": "Transport löschen", + "end": "Ende", + "ending_airport": "Endflughafen", + "flight_information": "Fluginformationen", + "from": "Aus", + "no_location_found": "Kein Standort gefunden", + "note_delete_confirm": "Sind Sie sicher, dass Sie diese Notiz löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.", + "out_of_range": "Nicht im Datumsbereich der Reiseroute", + "show_region_labels": "Regionsbeschriftungen anzeigen", + "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.", + "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", @@ -362,7 +384,9 @@ "item_cannot_be_empty": "Das Element darf nicht leer sein", "items": "Artikel", "new_item": "Neuer Artikel", - "save": "Speichern" + "save": "Speichern", + "checklist_viewer": "Checklisten-Viewer", + "new_checklist": "Neue Checkliste" }, "collection": { "collection_created": "Sammlung erfolgreich erstellt!", @@ -371,7 +395,8 @@ "edit_collection": "Sammlung bearbeiten", "error_creating_collection": "Fehler beim Erstellen der Sammlung", "error_editing_collection": "Fehler beim Bearbeiten der Sammlung", - "new_collection": "Neue Kollektion" + "new_collection": "Neue Kollektion", + "public_collection": "Öffentliche Sammlung" }, "notes": { "add_a_link": "Fügen Sie einen Link hinzu", @@ -384,7 +409,8 @@ "note_public": "Diese Notiz ist öffentlich, da sie sich in einer öffentlichen Sammlung befindet.", "open": "Offen", "save": "Speichern", - "invalid_url": "Ungültige URL" + "invalid_url": "Ungültige URL", + "note_viewer": "Notizenbetrachter" }, "transportation": { "date_and_time": "Datum", @@ -430,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", @@ -476,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 cd9b824..a7e668e 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -64,6 +64,12 @@ "no_image_found": "No image found", "collection_link_error": "Error linking adventure to collection", "adventure_delete_confirm": "Are you sure you want to delete this adventure? This action cannot be undone.", + "checklist_delete_confirm": "Are you sure you want to delete this checklist? This action cannot be undone.", + "note_delete_confirm": "Are you sure you want to delete this note? This action cannot be undone.", + "transportation_delete_confirm": "Are you sure you want to delete this transportation? This action cannot be undone.", + "delete_checklist": "Delete Checklist", + "delete_note": "Delete Note", + "delete_transportation": "Delete Transportation", "open_details": "Open Details", "edit_adventure": "Edit Adventure", "remove_from_collection": "Remove from Collection", @@ -97,6 +103,7 @@ "rating": "Rating", "my_images": "My Images", "add_an_activity": "Add an activity", + "show_region_labels": "Show Region Labels", "no_images": "No Images", "upload_images_here": "Upload images here", "share_adventure": "Share this Adventure!", @@ -216,8 +223,23 @@ "copy_failed": "Copy failed", "show": "Show", "hide": "Hide", + "clear_location": "Clear Location", + "starting_airport": "Starting Airport", + "ending_airport": "Ending Airport", + "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", + "flight_information": "Flight Information", + "out_of_range": "Not in itinerary date range", + "preview": "Preview", + "md_instructions": "Write your markdown here...", "days": "days", "activities": { "general": "General 🌍", @@ -271,7 +293,7 @@ "public_profile": "Public Profile", "public_tooltip": "With a public profile, users can share collections with you and view your profile on the users page.", "email_required": "Email is required", - "new_password": "New Password", + "new_password": "New Password (6+ characters)", "both_passwords_required": "Both passwords are required", "reset_failed": "Failed to reset password" }, @@ -356,7 +378,8 @@ "create": "Create", "collection_edit_success": "Collection edited successfully!", "error_editing_collection": "Error editing collection", - "edit_collection": "Edit Collection" + "edit_collection": "Edit Collection", + "public_collection": "Public Collection" }, "notes": { "note_deleted": "Note deleted successfully!", @@ -364,6 +387,7 @@ "open": "Open", "failed_to_save": "Failed to save note", "note_editor": "Note Editor", + "note_viewer": "Note Viewer", "editing_note": "Editing note", "content": "Content", "save": "Save", @@ -376,7 +400,9 @@ "checklist_delete_error": "Error deleting checklist", "failed_to_save": "Failed to save checklist", "checklist_editor": "Checklist Editor", + "checklist_viewer": "Checklist Viewer", "editing_checklist": "Editing checklist", + "new_checklist": "New Checklist", "item": "Item", "items": "Items", "add_item": "Add Item", @@ -442,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", @@ -476,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 42da49e..0211650 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -242,7 +242,29 @@ "emoji_picker": "Selector de emojis", "hide": "Esconder", "show": "Espectáculo", - "download_calendar": "Descargar Calendario" + "download_calendar": "Descargar Calendario", + "md_instructions": "Escriba su descuento aquí...", + "preview": "Avance", + "checklist_delete_confirm": "¿Está seguro de que desea eliminar esta lista de verificación? \nEsta acción no se puede deshacer.", + "clear_location": "Borrar ubicación", + "date_information": "Información de fecha", + "delete_checklist": "Eliminar lista de verificación", + "delete_note": "Eliminar nota", + "delete_transportation": "Eliminar transporte", + "end": "Fin", + "ending_airport": "Aeropuerto final", + "flight_information": "Información de vuelo", + "from": "De", + "no_location_found": "No se encontró ninguna ubicación", + "note_delete_confirm": "¿Estás seguro de que deseas eliminar esta nota? \nEsta acción no se puede deshacer.", + "out_of_range": "No en el rango de fechas del itinerario", + "show_region_labels": "Mostrar etiquetas de región", + "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.", + "show_map": "Mostrar mapa", + "will_be_marked": "se marcará como visitado una vez guardada la aventura." }, "worldtravel": { "all": "Todo", @@ -362,7 +384,9 @@ "item_cannot_be_empty": "El artículo no puede estar vacío", "items": "Elementos", "new_item": "Nuevo artículo", - "save": "Ahorrar" + "save": "Ahorrar", + "checklist_viewer": "Visor de lista de verificación", + "new_checklist": "Nueva lista de verificación" }, "collection": { "collection_created": "¡Colección creada con éxito!", @@ -371,7 +395,8 @@ "edit_collection": "Editar colección", "error_creating_collection": "Error al crear la colección", "error_editing_collection": "Error al editar la colección", - "new_collection": "Nueva colección" + "new_collection": "Nueva colección", + "public_collection": "Colección pública" }, "notes": { "add_a_link": "Agregar un enlace", @@ -384,7 +409,8 @@ "note_public": "Esta nota es pública porque está en una colección pública.", "open": "Abierto", "save": "Ahorrar", - "invalid_url": "URL no válida" + "invalid_url": "URL no válida", + "note_viewer": "Visor de notas" }, "transportation": { "date_and_time": "Fecha", @@ -442,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", @@ -476,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 9a532de..4572619 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -195,7 +195,29 @@ "emoji_picker": "Sélecteur d'émoticônes", "hide": "Cacher", "show": "Montrer", - "download_calendar": "Télécharger le calendrier" + "download_calendar": "Télécharger le calendrier", + "md_instructions": "Écrivez votre démarque ici...", + "preview": "Aperçu", + "checklist_delete_confirm": "Êtes-vous sûr de vouloir supprimer cette liste de contrôle ? \nCette action ne peut pas être annulée.", + "clear_location": "Effacer l'emplacement", + "date_information": "Informations sur les dates", + "delete_checklist": "Supprimer la liste de contrôle", + "delete_note": "Supprimer la note", + "delete_transportation": "Supprimer le transport", + "end": "Fin", + "ending_airport": "Aéroport de fin", + "flight_information": "Informations sur le vol", + "from": "Depuis", + "no_location_found": "Aucun emplacement trouvé", + "note_delete_confirm": "Êtes-vous sûr de vouloir supprimer cette note ? \nCette action ne peut pas être annulée.", + "out_of_range": "Pas dans la plage de dates de l'itinéraire", + "show_region_labels": "Afficher les étiquettes de région", + "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.", + "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é", @@ -362,7 +384,9 @@ "item_cannot_be_empty": "L'élément ne peut pas être vide", "items": "Articles", "new_item": "Nouvel article", - "save": "Sauvegarder" + "save": "Sauvegarder", + "checklist_viewer": "Visionneuse de liste de contrôle", + "new_checklist": "Nouvelle liste de contrôle" }, "collection": { "collection_created": "Collection créée avec succès !", @@ -371,7 +395,8 @@ "edit_collection": "Modifier la collection", "error_creating_collection": "Erreur lors de la création de la collection", "error_editing_collection": "Erreur lors de la modification de la collection", - "new_collection": "Nouvelle collection" + "new_collection": "Nouvelle collection", + "public_collection": "Collection publique" }, "notes": { "add_a_link": "Ajouter un lien", @@ -384,7 +409,8 @@ "note_public": "Cette note est publique car elle fait partie d'une collection publique.", "open": "Ouvrir", "save": "Sauvegarder", - "invalid_url": "URL invalide" + "invalid_url": "URL invalide", + "note_viewer": "Visionneuse de notes" }, "transportation": { "date_time": "Date de début", @@ -430,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", @@ -476,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 fa0b1fb..9c06a22 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -195,7 +195,29 @@ "emoji_picker": "Selettore di emoji", "hide": "Nascondere", "show": "Spettacolo", - "download_calendar": "Scarica Calendario" + "download_calendar": "Scarica Calendario", + "md_instructions": "Scrivi qui il tuo ribasso...", + "preview": "Anteprima", + "checklist_delete_confirm": "Sei sicuro di voler eliminare questa lista di controllo? \nQuesta azione non può essere annullata.", + "clear_location": "Cancella posizione", + "date_information": "Informazioni sulla data", + "delete_checklist": "Elimina lista di controllo", + "delete_note": "Elimina nota", + "delete_transportation": "Elimina trasporto", + "end": "FINE", + "ending_airport": "Fine dell'aeroporto", + "flight_information": "Informazioni sul volo", + "from": "Da", + "no_location_found": "Nessuna posizione trovata", + "note_delete_confirm": "Sei sicuro di voler eliminare questa nota? \nQuesta azione non può essere annullata.", + "out_of_range": "Non nell'intervallo di date dell'itinerario", + "show_region_labels": "Mostra etichette regione", + "start": "Inizio", + "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", + "will_be_marked": "verrà contrassegnato come visitato una volta salvata l'avventura." }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", @@ -362,7 +384,9 @@ "item_cannot_be_empty": "L'articolo non può essere vuoto", "items": "Elementi", "save": "Salva", - "new_item": "Nuovo articolo" + "new_item": "Nuovo articolo", + "checklist_viewer": "Visualizzatore della lista di controllo", + "new_checklist": "Nuova lista di controllo" }, "collection": { "edit_collection": "Modifica raccolta", @@ -371,7 +395,8 @@ "new_collection": "Nuova collezione", "collection_created": "Collezione creata con successo!", "collection_edit_success": "Raccolta modificata con successo!", - "create": "Creare" + "create": "Creare", + "public_collection": "Collezione pubblica" }, "notes": { "add_a_link": "Aggiungi un collegamento", @@ -384,7 +409,8 @@ "note_public": "Questa nota è pubblica perché è in una collezione pubblica.", "open": "Aprire", "save": "Salva", - "invalid_url": "URL non valido" + "invalid_url": "URL non valido", + "note_viewer": "Visualizzatore di note" }, "transportation": { "date_and_time": "Data", @@ -430,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", @@ -476,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 90ac331..91fb42b 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -195,7 +195,29 @@ "emoji_picker": "Emoji-kiezer", "hide": "Verbergen", "show": "Show", - "download_calendar": "Agenda downloaden" + "download_calendar": "Agenda downloaden", + "md_instructions": "Schrijf hier uw korting...", + "preview": "Voorbeeld", + "checklist_delete_confirm": "Weet u zeker dat u deze checklist wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.", + "clear_location": "Locatie wissen", + "date_information": "Datuminformatie", + "delete_checklist": "Controlelijst verwijderen", + "delete_note": "Notitie verwijderen", + "delete_transportation": "Transport verwijderen", + "end": "Einde", + "flight_information": "Vluchtinformatie", + "from": "Van", + "no_location_found": "Geen locatie gevonden", + "note_delete_confirm": "Weet u zeker dat u deze notitie wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.", + "out_of_range": "Niet binnen het datumbereik van het reisplan", + "show_region_labels": "Toon regiolabels", + "start": "Begin", + "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", + "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", @@ -362,7 +384,9 @@ "item_cannot_be_empty": "Artikel mag niet leeg zijn", "items": "Artikelen", "new_item": "Nieuw artikel", - "save": "Opslaan" + "save": "Opslaan", + "checklist_viewer": "Controlelijstviewer", + "new_checklist": "Nieuwe checklist" }, "collection": { "collection_created": "Collectie succesvol aangemaakt!", @@ -371,7 +395,8 @@ "edit_collection": "Collectie bewerken", "error_creating_collection": "Fout bij aanmaken collectie", "error_editing_collection": "Fout bij bewerken collectie", - "new_collection": "Nieuwe collectie" + "new_collection": "Nieuwe collectie", + "public_collection": "Openbare collectie" }, "notes": { "add_a_link": "Voeg een link toe", @@ -384,7 +409,8 @@ "note_public": "Deze opmerking is openbaar omdat deze zich in een openbare collectie bevindt.", "open": "Open", "save": "Opslaan", - "invalid_url": "Ongeldige URL" + "invalid_url": "Ongeldige URL", + "note_viewer": "Notitieviewer" }, "transportation": { "date_and_time": "Datum", @@ -430,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", @@ -476,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 db3e758..641aa95 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -242,7 +242,29 @@ "emoji_picker": "Wybór emoji", "hide": "Ukrywać", "show": "Pokazywać", - "download_calendar": "Pobierz Kalendarz" + "download_calendar": "Pobierz Kalendarz", + "md_instructions": "Napisz tutaj swoją przecenę...", + "preview": "Zapowiedź", + "checklist_delete_confirm": "Czy na pewno chcesz usunąć tę listę kontrolną? \nTej akcji nie można cofnąć.", + "clear_location": "Wyczyść lokalizację", + "date_information": "Informacje o dacie", + "delete_checklist": "Usuń listę kontrolną", + "delete_note": "Usuń notatkę", + "delete_transportation": "Usuń transport", + "end": "Koniec", + "ending_airport": "Kończy się lotnisko", + "flight_information": "Informacje o locie", + "from": "Z", + "no_location_found": "Nie znaleziono lokalizacji", + "note_delete_confirm": "Czy na pewno chcesz usunąć tę notatkę? \nTej akcji nie można cofnąć.", + "out_of_range": "Nie mieści się w zakresie dat planu podróży", + "show_region_labels": "Pokaż etykiety regionów", + "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ąć.", + "show_map": "Pokaż mapę", + "will_be_marked": "zostanie oznaczona jako odwiedzona po zapisaniu przygody." }, "worldtravel": { "country_list": "Lista krajów", @@ -356,7 +378,8 @@ "create": "Utwórz", "collection_edit_success": "Kolekcja została pomyślnie edytowana!", "error_editing_collection": "Błąd podczas edytowania kolekcji", - "edit_collection": "Edytuj kolekcję" + "edit_collection": "Edytuj kolekcję", + "public_collection": "Kolekcja publiczna" }, "notes": { "note_deleted": "Notatka została pomyślnie usunięta!", @@ -369,7 +392,8 @@ "save": "Zapisz", "note_public": "Ta notatka jest publiczna, ponieważ znajduje się w publicznej kolekcji.", "add_a_link": "Dodaj link", - "invalid_url": "Nieprawidłowy URL" + "invalid_url": "Nieprawidłowy URL", + "note_viewer": "Przeglądarka notatek" }, "checklist": { "checklist_deleted": "Lista kontrolna została pomyślnie usunięta!", @@ -384,7 +408,9 @@ "save": "Zapisz", "checklist_public": "Ta lista kontrolna jest publiczna, ponieważ znajduje się w publicznej kolekcji.", "item_cannot_be_empty": "Element nie może być pusty", - "item_already_exists": "Element już istnieje" + "item_already_exists": "Element już istnieje", + "checklist_viewer": "Przeglądarka listy kontrolnej", + "new_checklist": "Nowa lista kontrolna" }, "transportation": { "transportation_deleted": "Transport został pomyślnie usunięty!", @@ -442,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", @@ -476,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 aae55d5..505fbb6 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -1,9 +1,9 @@ { "about": { "about": "Om", - "close": "Nära", + "close": "Stäng", "license": "Licensierad under GPL-3.0-licensen.", - "message": "Tillverkad med ❤️ i USA.", + "message": "Skapat med ❤️ i USA.", "nominatim_1": "Platssökning och geokodning tillhandahålls av", "nominatim_2": "Deras data är licensierad under ODbL-licensen.", "oss_attributions": "Tillskrivningar med öppen källkod", @@ -80,12 +80,12 @@ "not_found_desc": "Äventyret du letade efter kunde inte hittas. \nProva ett annat äventyr eller kom tillbaka senare.", "open_details": "Öppna Detaljer", "open_filters": "Öppna filter", - "order_by": "Beställ efter", - "order_direction": "Beställ riktning", - "planned": "Planerad", + "order_by": "Sortera efter", + "order_direction": "Sorteringsriktning", + "planned": "Planerade", "private": "Privat", "public": "Offentlig", - "rating": "Gradering", + "rating": "Betyg", "remove_from_collection": "Ta bort från samlingen", "share": "Dela", "sort": "Sortera", @@ -93,7 +93,7 @@ "unarchive": "Avarkivera", "unarchived_collection_message": "Samlingen har tagits bort från arkivet!", "visit": "Besök", - "visited": "Besökte", + "visited": "Besökta", "visits": "Besök", "image_removed_error": "Det gick inte att ta bort bilden", "image_removed_success": "Bilden har tagits bort!", @@ -153,12 +153,12 @@ "all": "Alla", "error_updating_regions": "Fel vid uppdatering av regioner", "mark_region_as_visited": "Markera region {region}, {country} som besökt?", - "mark_visited": "Mark besökte", + "mark_visited": "Markera som besökt", "my_adventures": "Mina äventyr", "no_adventures_found": "Inga äventyr hittades", "no_collections_found": "Inga samlingar hittades att lägga till detta äventyr till.", "no_linkable_adventures": "Inga äventyr hittades som kan kopplas till denna samling.", - "not_visited": "Ej besökt", + "not_visited": "Ej besökta", "regions_updated": "regioner uppdaterade", "update_visited_regions": "Uppdatera besökta regioner", "update_visited_regions_disclaimer": "Detta kan ta ett tag beroende på antalet äventyr du har besökt.", @@ -195,7 +195,29 @@ "emoji_picker": "Emoji-väljare", "hide": "Dölja", "show": "Visa", - "download_calendar": "Ladda ner kalender" + "download_calendar": "Ladda ner kalender", + "md_instructions": "Skriv din avskrivning här...", + "preview": "Förhandsvisning", + "checklist_delete_confirm": "Är du säker på att du vill ta bort den här checklistan? \nDenna åtgärd kan inte ångras.", + "clear_location": "Rensa plats", + "date_information": "Datuminformation", + "delete_checklist": "Ta bort checklista", + "delete_note": "Ta bort anteckning", + "delete_transportation": "Ta bort Transport", + "end": "Avsluta", + "ending_airport": "Slutar flygplats", + "flight_information": "Flyginformation", + "from": "Från", + "no_location_found": "Ingen plats hittades", + "note_delete_confirm": "Är du säker på att du vill ta bort den här anteckningen? \nDenna åtgärd kan inte ångras.", + "out_of_range": "Inte inom resplanens datumintervall", + "show_region_labels": "Visa regionetiketter", + "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.", + "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", @@ -203,29 +225,29 @@ "feature_1": "Reselogg", "feature_1_desc": "Håll koll på dina äventyr med en personlig reselogg och dela dina upplevelser med vänner och familj.", "feature_2": "Reseplanering", - "feature_2_desc": "Skapa enkelt anpassade resplaner och få en uppdelning av din resa dag för dag.", + "feature_2_desc": "Skapa enkelt skräddarsydda resplaner och få en översikt över din resa, dag för dag.", "feature_3": "Resekarta", "feature_3_desc": "Se dina resor över hela världen med en interaktiv karta och utforska nya destinationer.", "go_to": "Gå till AdventureLog", "hero_1": "Upptäck världens mest spännande äventyr", "hero_2": "Upptäck och planera ditt nästa äventyr med AdventureLog. \nUtforska hisnande destinationer, skapa anpassade resplaner och håll kontakten när du är på språng.", - "key_features": "Nyckelfunktioner" + "key_features": "Viktiga funktioner" }, "navbar": { "about": "Om AdventureLog", "adventures": "Äventyr", "collections": "Samlingar", - "discord": "Disharmoni", + "discord": "Discord", "documentation": "Dokumentation", "greeting": "Hej", - "logout": "Utloggning", + "logout": "Logga ut", "map": "Karta", "my_adventures": "Mina äventyr", "profile": "Profil", - "search": "Söka", + "search": "Sök", "settings": "Inställningar", "shared_with_me": "Delade med mig", - "theme_selection": "Temaval", + "theme_selection": "Tema", "themes": { "aqua": "Aqua", "dark": "Mörk", @@ -239,21 +261,21 @@ "users": "Användare", "worldtravel": "Världsresor", "my_tags": "Mina taggar", - "tag": "Märka", + "tag": "Tagg", "language_selection": "Språk", - "support": "Stöd", + "support": "Support", "calendar": "Kalender" }, "worldtravel": { "all": "Alla", "all_subregions": "Alla underregioner", "clear_search": "Rensa sökning", - "completely_visited": "Helt besökt", + "completely_visited": "Fullständigt besökta", "country_list": "Lista över länder", "no_countries_found": "Inga länder hittades", - "not_visited": "Ej besökt", - "num_countries": "hittade länder", - "partially_visited": "Delvis besökt" + "not_visited": "Ej besökta", + "num_countries": "länder hittades", + "partially_visited": "Delvis besökta" }, "auth": { "confirm_password": "Bekräfta lösenord", @@ -261,7 +283,7 @@ "first_name": "Förnamn", "forgot_password": "Glömt lösenordet?", "last_name": "Efternamn", - "login": "Inloggning", + "login": "Logga in", "login_error": "Det går inte att logga in med de angivna uppgifterna.", "password": "Lösenord", "registration_disabled": "Registreringen är för närvarande inaktiverad.", @@ -287,25 +309,25 @@ "new_password": "Nytt lösenord", "no_email_set": "Ingen e-post inställd", "password_change": "Ändra lösenord", - "settings_page": "Inställningssida", + "settings_page": "Inställningar", "update": "Uppdatera", - "update_error": "Fel vid uppdatering av inställningar", + "update_error": "Ett fel uppstod vid uppdatering av inställningar", "update_success": "Inställningarna har uppdaterats!", "change_password": "Ändra lösenord", "invalid_token": "Token är ogiltig eller har gått ut", "login_redir": "Du kommer då att omdirigeras till inloggningssidan.", "missing_email": "Vänligen ange en e-postadress", - "password_does_not_match": "Lösenord stämmer inte överens", + "password_does_not_match": "Lösenorden stämmer inte överens", "password_is_required": "Lösenord krävs", "possible_reset": "Om e-postadressen du angav är kopplad till ett konto kommer du att få ett e-postmeddelande med instruktioner för att återställa ditt lösenord!", "reset_password": "Återställ lösenord", - "submit": "Överlämna", + "submit": "Skicka", "token_required": "Token och UID krävs för lösenordsåterställning.", "about_this_background": "Om denna bakgrund", "join_discord": "Gå med i Discord", "join_discord_desc": "för att dela dina egna foton. \nLägg upp dem i", "photo_by": "Foto av", - "change_password_error": "Det går inte att ändra lösenord. \nOgiltigt nuvarande lösenord eller ogiltigt nytt lösenord.", + "change_password_error": "Det gick inte att ändra lösenord. \nDet nuvarande eller det nya lösenordet är ogiltigt.", "current_password": "Aktuellt lösenord", "password_change_lopout_warning": "Du kommer att loggas ut efter att du har ändrat ditt lösenord.", "authenticator_code": "Autentiseringskod", @@ -351,18 +373,20 @@ }, "checklist": { "add_item": "Lägg till objekt", - "checklist_delete_error": "Fel vid borttagning av checklista", + "checklist_delete_error": "Ett fel uppstod vid borttagning av checklista", "checklist_deleted": "Checklistan har raderats!", - "checklist_editor": "Checklista Editor", + "checklist_editor": "Redigerare för checklistor", "checklist_public": "Den här checklistan är offentlig eftersom den finns i en offentlig samling.", - "editing_checklist": "Redigeringschecklista", + "editing_checklist": "Redigerar checklista", "failed_to_save": "Det gick inte att spara checklistan", "item": "Punkt", - "item_already_exists": "Objektet finns redan", - "item_cannot_be_empty": "Objektet får inte vara tomt", - "items": "Föremål", + "item_already_exists": "Listobjektet finns redan", + "item_cannot_be_empty": "Listobjektet får inte vara tomt", + "items": "Punkter", "new_item": "Nytt föremål", - "save": "Spara" + "save": "Spara", + "checklist_viewer": "Se Checklista", + "new_checklist": "Ny checklista" }, "collection": { "collection_created": "Samlingen har skapats!", @@ -370,21 +394,23 @@ "create": "Skapa", "edit_collection": "Redigera samling", "error_creating_collection": "Det gick inte att skapa samlingen", - "error_editing_collection": "Fel vid redigering av samling", - "new_collection": "Ny samling" + "error_editing_collection": "Ett fel uppstod vid redigering av samling", + "new_collection": "Ny samling", + "public_collection": "Offentlig samling" }, "notes": { "add_a_link": "Lägg till en länk", "content": "Innehåll", - "editing_note": "Redigeringsanteckning", + "editing_note": "Redigerar anteckning", "failed_to_save": "Det gick inte att spara anteckningen", "note_delete_error": "Det gick inte att ta bort anteckningen", "note_deleted": "Anteckningen har raderats!", - "note_editor": "Note Editor", + "note_editor": "Redigerare för anteckningar", "note_public": "Den här anteckningen är offentlig eftersom den finns i en offentlig samling.", "open": "Öppna", "save": "Spara", - "invalid_url": "Ogiltig URL" + "invalid_url": "Ogiltig URL", + "note_viewer": "Note Viewer" }, "transportation": { "date_and_time": "Datum", @@ -401,7 +427,7 @@ "bus": "Buss", "car": "Bil", "other": "Andra", - "plane": "Plan", + "plane": "Flygplan", "train": "Tåg", "walking": "Gående" }, @@ -430,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": "polsk" - }, + "languages": {}, "share": { "no_users_shared": "Inga användare delas med", "not_shared_with": "Inte delad med", @@ -451,13 +467,13 @@ "with": "med", "go_to_settings": "Gå till inställningar", "no_shared_found": "Inga samlingar hittades som delas med dig.", - "set_public": "För att tillåta användare att dela med dig måste du ha din profil inställd på offentlig." + "set_public": "För att tillåta användare att dela med dig måste du ha en offentlig profil." }, "profile": { - "member_since": "Medlem sedan dess", + "member_since": "Medlem sedan", "user_stats": "Användarstatistik", "visited_countries": "Besökta länder", - "visited_regions": "Besökte regioner" + "visited_regions": "Besökta regioner" }, "categories": { "category_name": "Kategorinamn", @@ -476,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 56cf01f..86674c5 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -195,7 +195,29 @@ "emoji_picker": "表情符号选择器", "hide": "隐藏", "show": "展示", - "download_calendar": "下载日历" + "download_calendar": "下载日历", + "md_instructions": "在这里写下你的标记...", + "preview": "预览", + "checklist_delete_confirm": "您确定要删除此清单吗?\n此操作无法撤消。", + "clear_location": "明确的位置", + "date_information": "日期信息", + "delete_checklist": "删除清单", + "delete_note": "删除注释", + "delete_transportation": "删除交通", + "end": "结尾", + "ending_airport": "结束机场", + "flight_information": "航班信息", + "from": "从", + "no_location_found": "没有找到位置", + "note_delete_confirm": "您确定要删除此注释吗?\n此操作无法撤消。", + "out_of_range": "不在行程日期范围内", + "show_region_labels": "显示区域标签", + "start": "开始", + "starting_airport": "出发机场", + "to": "到", + "transportation_delete_confirm": "您确定要删除此交通工具吗?\n此操作无法撤消。", + "show_map": "显示地图", + "will_be_marked": "保存冒险后将被标记为已访问。" }, "home": { "desc_1": "轻松发现、规划和探索", @@ -362,7 +384,9 @@ "item_cannot_be_empty": "项目不能为空", "items": "项目", "new_item": "新商品", - "save": "节省" + "save": "节省", + "checklist_viewer": "清单查看器", + "new_checklist": "新清单" }, "collection": { "collection_created": "收藏创建成功!", @@ -371,7 +395,8 @@ "edit_collection": "编辑收藏", "error_creating_collection": "创建集合时出错", "error_editing_collection": "编辑集合时出错", - "new_collection": "新系列" + "new_collection": "新系列", + "public_collection": "公共收藏" }, "notes": { "add_a_link": "添加链接", @@ -384,7 +409,8 @@ "note_public": "该笔记是公开的,因为它属于公共收藏。", "open": "打开", "save": "节省", - "invalid_url": "无效网址" + "invalid_url": "无效网址", + "note_viewer": "笔记查看器" }, "transportation": { "date_and_time": "日期", @@ -430,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": "不与共享", @@ -476,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/+page.server.ts b/frontend/src/routes/+page.server.ts index 8d0446a..b379a8c 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -42,6 +42,7 @@ export const actions: Actions = { credentials: 'include' }); if (res.status == 401) { + event.cookies.delete('sessionid', { path: '/', secure: event.url.protocol === 'https:' }); return redirect(302, '/login'); } else { return redirect(302, '/'); diff --git a/frontend/src/routes/adventures/[id]/+page.svelte b/frontend/src/routes/adventures/[id]/+page.svelte index 9e357ea..21b622f 100644 --- a/frontend/src/routes/adventures/[id]/+page.svelte +++ b/frontend/src/routes/adventures/[id]/+page.svelte @@ -6,6 +6,11 @@ import Lost from '$lib/assets/undraw_lost.svg'; import { DefaultMarker, MapLibre, Popup } from 'svelte-maplibre'; import { t } from 'svelte-i18n'; + import { marked } from 'marked'; // Import the markdown parser + + const renderMarkdown = (markdown: string) => { + return marked(markdown); + }; export let data: PageData; console.log(data); @@ -29,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; } @@ -244,11 +259,12 @@ {/if}
    {#if adventure.description} -
    -

    - {adventure.description} -

    -
    +

    +
    + {@html renderMarkdown(adventure.description)} +
    {/if}
    @@ -323,7 +339,7 @@
    { }> = []; adventures.forEach((adventure) => { adventure.visits.forEach((visit) => { - dates.push({ - id: adventure.id, - start: visit.start_date, - end: visit.end_date || visit.start_date, - title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '') - }); + if (visit.start_date) { + dates.push({ + id: adventure.id, + start: visit.start_date, + end: visit.end_date || visit.start_date, + title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '') + }); + } }); }); diff --git a/frontend/src/routes/calendar/+page.svelte b/frontend/src/routes/calendar/+page.svelte index 476519d..50987c2 100644 --- a/frontend/src/routes/calendar/+page.svelte +++ b/frontend/src/routes/calendar/+page.svelte @@ -23,6 +23,7 @@ view: 'dayGridMonth', events: [...dates] }; + console.log(dates);

    {$t('adventures.adventure_calendar')}

    diff --git a/frontend/src/routes/collections/+page.svelte b/frontend/src/routes/collections/+page.svelte index e5789c9..171a5d4 100644 --- a/frontend/src/routes/collections/+page.svelte +++ b/frontend/src/routes/collections/+page.svelte @@ -2,8 +2,8 @@ import { enhance } from '$app/forms'; import { goto } from '$app/navigation'; import CollectionCard from '$lib/components/CollectionCard.svelte'; - import EditCollection from '$lib/components/EditCollection.svelte'; - import NewCollection from '$lib/components/NewCollection.svelte'; + import CollectionLink from '$lib/components/CollectionLink.svelte'; + import CollectionModal from '$lib/components/CollectionModal.svelte'; import NotFound from '$lib/components/NotFound.svelte'; import type { Collection } from '$lib/types'; import { t } from 'svelte-i18n'; @@ -17,10 +17,10 @@ let currentSort = { attribute: 'name', order: 'asc' }; - let isShowingCreateModal: boolean = false; let newType: string = ''; let resultsPerPage: number = 25; + let isShowingCollectionModal: boolean = false; let next: string | null = data.props.next || null; let previous: string | null = data.props.previous || null; @@ -74,33 +74,37 @@ collections = collections.filter((collection) => collection.id !== event.detail); } - function sort({ attribute, order }: { attribute: string; order: string }) { - currentSort.attribute = attribute; - currentSort.order = order; - if (attribute === 'name') { - if (order === 'asc') { - collections = collections.sort((a, b) => b.name.localeCompare(a.name)); - } else { - collections = collections.sort((a, b) => a.name.localeCompare(b.name)); - } + // function sort({ attribute, order }: { attribute: string; order: string }) { + // currentSort.attribute = attribute; + // currentSort.order = order; + // if (attribute === 'name') { + // if (order === 'asc') { + // collections = collections.sort((a, b) => b.name.localeCompare(a.name)); + // } else { + // collections = collections.sort((a, b) => a.name.localeCompare(b.name)); + // } + // } + // } + + let collectionToEdit: Collection | null = null; + + function saveOrCreate(event: CustomEvent) { + if (collections.find((collection) => collection.id === event.detail.id)) { + collections = collections.map((collection) => { + if (collection.id === event.detail.id) { + return event.detail; + } + return collection; + }); + } else { + collections = [event.detail, ...collections]; } - } - - let collectionToEdit: Collection; - let isEditModalOpen: boolean = false; - - function deleteAdventure(event: CustomEvent) { - collections = collections.filter((adventure) => adventure.id !== event.detail); - } - - function createAdventure(event: CustomEvent) { - collections = [event.detail, ...collections]; - isShowingCreateModal = false; + isShowingCollectionModal = false; } function editCollection(event: CustomEvent) { collectionToEdit = event.detail; - isEditModalOpen = true; + isShowingCollectionModal = true; } function saveEdit(event: CustomEvent) { @@ -110,7 +114,7 @@ } return adventure; }); - isEditModalOpen = false; + isShowingCollectionModal = false; } let sidebarOpen = false; @@ -120,18 +124,14 @@ } -{#if isShowingCreateModal} - (isShowingCreateModal = false)} /> -{/if} - -{#if isEditModalOpen} - (isEditModalOpen = false)} + on:close={() => (isShowingCollectionModal = false)} on:saveEdit={saveEdit} + on:save={saveOrCreate} /> {/if} -
    @@ -267,6 +263,6 @@
    - {$t(`navbar.collections`)} + Collections diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 10bc85b..c4107b8 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -2,18 +2,23 @@ import type { Adventure, Checklist, Collection, Note, Transportation } from '$lib/types'; import { onMount } from 'svelte'; import type { PageData } from './$types'; - import { goto } from '$app/navigation'; - import Lost from '$lib/assets/undraw_lost.svg'; + import { marked } from 'marked'; // Import the markdown parser + import { t } from 'svelte-i18n'; + // @ts-ignore + import Calendar from '@event-calendar/core'; + // @ts-ignore + import TimeGrid from '@event-calendar/time-grid'; + // @ts-ignore + import DayGrid from '@event-calendar/day-grid'; + import Plus from '~icons/mdi/plus'; import AdventureCard from '$lib/components/AdventureCard.svelte'; import AdventureLink from '$lib/components/AdventureLink.svelte'; import NotFound from '$lib/components/NotFound.svelte'; - import { DefaultMarker, MapLibre, Popup } from 'svelte-maplibre'; + import { DefaultMarker, MapLibre, Marker, Popup } from 'svelte-maplibre'; import TransportationCard from '$lib/components/TransportationCard.svelte'; - import EditTransportation from '$lib/components/EditTransportation.svelte'; - import NewTransportation from '$lib/components/NewTransportation.svelte'; import NoteCard from '$lib/components/NoteCard.svelte'; import NoteModal from '$lib/components/NoteModal.svelte'; @@ -26,12 +31,79 @@ import ChecklistCard from '$lib/components/ChecklistCard.svelte'; import ChecklistModal from '$lib/components/ChecklistModal.svelte'; import AdventureModal from '$lib/components/AdventureModal.svelte'; + import TransportationModal from '$lib/components/TransportationModal.svelte'; export let data: PageData; console.log(data); + const renderMarkdown = (markdown: string) => { + return marked(markdown); + }; + let collection: Collection; + // add christmas and new years + // dates = Array.from({ length: 25 }, (_, i) => { + // const date = new Date(); + // date.setMonth(11); + // date.setDate(i + 1); + // return { + // id: i.toString(), + // start: date.toISOString(), + // end: date.toISOString(), + // title: '🎄' + // }; + // }); + + let dates: Array<{ + id: string; + start: string; + end: string; + title: string; + backgroundColor?: string; + }> = []; + + // Initialize calendar plugins and options + let plugins = [TimeGrid, DayGrid]; + let options = { + view: 'dayGridMonth', + events: dates // Assign `dates` reactively + }; + + // Compute `dates` array reactively + $: { + dates = []; + + if (adventures) { + dates = dates.concat( + adventures.flatMap((adventure) => + adventure.visits.map((visit) => ({ + id: adventure.id, + start: visit.start_date || '', // Ensure it's a string + end: visit.end_date || visit.start_date || '', // Ensure it's a string + title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '') + })) + ) + ); + } + + if (transportations) { + dates = dates.concat( + transportations.map((transportation) => ({ + id: transportation.id, + start: transportation.date || '', // Ensure it's a string + end: transportation.end_date || transportation.date || '', // Ensure it's a string + title: transportation.name + (transportation.type ? ` (${transportation.type})` : '') + })) + ); + } + + // Update `options.events` when `dates` changes + options = { ...options, events: dates }; + } + + let currentView: string = 'itinerary'; + let adventures: Adventure[] = []; let numVisited: number = 0; @@ -43,6 +115,29 @@ let numberOfDays: number = NaN; + function getTransportationEmoji(type: string): string { + switch (type) { + case 'car': + return '🚗'; + case 'plane': + return '✈️'; + case 'train': + return '🚆'; + case 'bus': + return '🚌'; + case 'boat': + return '⛵'; + case 'bike': + return '🚲'; + case 'walking': + return '🚶'; + case 'other': + return '🚀'; + default: + return '🚀'; + } + } + $: { numAdventures = adventures.length; numVisited = adventures.filter((adventure) => adventure.is_visited).length; @@ -76,6 +171,11 @@ if (collection.checklists) { checklists = collection.checklists; } + if (!collection.start_date) { + currentView = 'all'; + } else { + currentView = 'itinerary'; + } }); function deleteAdventure(event: CustomEvent) { @@ -108,9 +208,8 @@ } let adventureToEdit: Adventure | null = null; - let transportationToEdit: Transportation; + let transportationToEdit: Transportation | null = null; let isAdventureModalOpen: boolean = false; - let isTransportationEditModalOpen: boolean = false; let isNoteModalOpen: boolean = false; let noteToEdit: Note | null; let checklistToEdit: Checklist | null; @@ -122,17 +221,12 @@ isAdventureModalOpen = true; } - function saveNewTransportation(event: CustomEvent) { - transportations = transportations.map((transportation) => { - if (transportation.id === event.detail.id) { - return event.detail; - } - return transportation; - }); - isTransportationEditModalOpen = false; + function editTransportation(event: CustomEvent) { + transportationToEdit = event.detail; + isShowingTransportationModal = true; } - function saveOrCreate(event: CustomEvent) { + function saveOrCreateAdventure(event: CustomEvent) { if (adventures.find((adventure) => adventure.id === event.detail.id)) { adventures = adventures.map((adventure) => { if (adventure.id === event.detail.id) { @@ -145,6 +239,22 @@ } isAdventureModalOpen = false; } + + function saveOrCreateTransportation(event: CustomEvent) { + if (transportations.find((transportation) => transportation.id === event.detail.id)) { + // Update existing transportation + transportations = transportations.map((transportation) => { + if (transportation.id === event.detail.id) { + return event.detail; + } + return transportation; + }); + } else { + // Create new transportation + transportations = [event.detail, ...transportations]; + } + isShowingTransportationModal = false; + } {#if isShowingLinkModal} @@ -157,13 +267,12 @@ /> {/if} -{#if isTransportationEditModalOpen} - (isTransportationEditModalOpen = false)} - on:saveEdit={saveNewTransportation} - startDate={collection.start_date} - endDate={collection.end_date} + on:close={() => (isShowingTransportationModal = false)} + on:save={saveOrCreateTransportation} + {collection} /> {/if} @@ -171,7 +280,7 @@ (isAdventureModalOpen = false)} - on:save={saveOrCreate} + on:save={saveOrCreateAdventure} {collection} /> {/if} @@ -221,49 +330,13 @@ /> {/if} -{#if isShowingTransportationModal} - (isShowingTransportationModal = false)} - on:add={(event) => { - transportations = [event.detail, ...transportations]; - isShowingTransportationModal = false; - }} - {collection} - startDate={collection.start_date} - endDate={collection.end_date} - /> -{/if} - -{#if notFound} -
    -
    -
    - Lost -
    -

    - {$t('adventures.not_found')} -

    -

    - {$t('adventures.not_found_desc')} -

    -
    - -
    -
    -
    -{/if} - {#if !collection && !notFound}
    {/if} {#if collection} - {#if data.user && data.user.uuid && (data.user.uuid == collection.user_id || collection.shared_with.includes(data.user.uuid)) && !collection.is_archived} + {#if data.user && data.user.uuid && (data.user.uuid == collection.user_id || (collection.shared_with && collection.shared_with.includes(data.user.uuid))) && !collection.is_archived}
    {/if} - {#if collection.description} -

    {collection.description}

    + {#if collection && !collection.start_date && adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0} + {/if} + + {#if collection.description} +
    +
    + {@html renderMarkdown(collection.description)} +
    +
    + {/if} + {#if adventures.length > 0}
    @@ -386,219 +473,332 @@
    {/if} - {#if adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0} - - {/if} - {#if adventures.length > 0} -

    {$t('adventures.linked_adventures')}

    - -
    - {#each adventures as adventure} - - {/each} + {#if collection.id} + {/if} - {#if transportations.length > 0} -

    {$t('adventures.transportations')}

    -
    - {#each transportations as transportation} - { - transportations = transportations.filter((t) => t.id != event.detail); - }} - on:edit={(event) => { - transportationToEdit = event.detail; - isTransportationEditModalOpen = true; - }} - {collection} - /> - {/each} -
    - {/if} + {#if currentView == 'all'} + {#if adventures.length > 0} +

    {$t('adventures.linked_adventures')}

    - {#if notes.length > 0} -

    {$t('adventures.notes')}

    -
    - {#each notes as note} - { - noteToEdit = event.detail; - isNoteModalOpen = true; - }} - on:delete={(event) => { - notes = notes.filter((n) => n.id != event.detail); - }} - {collection} - /> - {/each} -
    - {/if} +
    + {#each adventures as adventure} + + {/each} +
    + {/if} - {#if checklists.length > 0} -

    {$t('adventures.checklists')}

    -
    - {#each checklists as checklist} - { - checklists = checklists.filter((n) => n.id != event.detail); - }} - on:edit={(event) => { - checklistToEdit = event.detail; - isShowingChecklistModal = true; - }} - {collection} - /> - {/each} -
    + {#if transportations.length > 0} +

    {$t('adventures.transportations')}

    +
    + {#each transportations as transportation} + { + transportations = transportations.filter((t) => t.id != event.detail); + }} + on:edit={editTransportation} + {collection} + /> + {/each} +
    + {/if} + + {#if notes.length > 0} +

    {$t('adventures.notes')}

    +
    + {#each notes as note} + { + noteToEdit = event.detail; + isNoteModalOpen = true; + }} + on:delete={(event) => { + notes = notes.filter((n) => n.id != event.detail); + }} + {collection} + /> + {/each} +
    + {/if} + + {#if checklists.length > 0} +

    {$t('adventures.checklists')}

    +
    + {#each checklists as checklist} + { + checklists = checklists.filter((n) => n.id != event.detail); + }} + on:edit={(event) => { + checklistToEdit = event.detail; + isShowingChecklistModal = true; + }} + {collection} + /> + {/each} +
    + {/if} + + + {#if adventures.length == 0 && transportations.length == 0 && notes.length == 0 && checklists.length == 0} + + {/if} {/if} {#if collection.start_date && collection.end_date} -
    -

    {$t('adventures.itineary_by_date')}

    - {#if numberOfDays} -

    - {$t('adventures.duration')}: {numberOfDays} - {$t('adventures.days')} -

    - {/if} -

    - Dates: {new Date(collection.start_date).toLocaleDateString(undefined, { timeZone: 'UTC' })} - {new Date( - collection.end_date - ).toLocaleDateString(undefined, { timeZone: 'UTC' })} -

    - - {#each Array(numberOfDays) as _, i} - {@const startDate = new Date(collection.start_date)} - {@const tempDate = new Date(startDate.getTime())} - - {@const adjustedDate = new Date(tempDate.setUTCDate(tempDate.getUTCDate() + i))} - - {@const dateString = adjustedDate.toISOString().split('T')[0]} - - {@const dayAdventures = - groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays)[ - dateString - ] || []} - - {@const dayTransportations = - groupTransportationsByDate(transportations, new Date(collection.start_date), numberOfDays)[ - dateString - ] || []} - - {@const dayNotes = - groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] || []} - - {@const dayChecklists = - groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays)[ - dateString - ] || []} - -

    - {$t('adventures.day')} - {i + 1} -

    -

    - {adjustedDate.toLocaleDateString(undefined, { timeZone: 'UTC' })} -

    -
    - {#if dayAdventures.length > 0} - {#each dayAdventures as adventure} - - {/each} - {/if} - {#if dayTransportations.length > 0} - {#each dayTransportations as transportation} - { - transportations = transportations.filter((t) => t.id != event.detail); - }} - on:edit={(event) => { - transportationToEdit = event.detail; - isTransportationEditModalOpen = true; - }} - /> - {/each} - {/if} - {#if dayNotes.length > 0} - {#each dayNotes as note} - { - noteToEdit = event.detail; - isNoteModalOpen = true; - }} - on:delete={(event) => { - notes = notes.filter((n) => n.id != event.detail); - }} - /> - {/each} - {/if} - {#if dayChecklists.length > 0} - {#each dayChecklists as checklist} - { - notes = notes.filter((n) => n.id != event.detail); - }} - on:edit={(event) => { - checklistToEdit = event.detail; - isShowingChecklistModal = true; - }} - /> - {/each} - {/if} - - {#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0} -

    {$t('adventures.nothing_planned')}

    - {/if} -
    - {/each} - - - - - - {#each adventures as adventure} - {#if adventure.longitude && adventure.latitude} - - -
    {adventure.name}
    -

    - {adventure.category?.display_name + ' ' + adventure.category?.icon} + {#if currentView == 'itinerary'} +

    +
    +
    +

    {$t('adventures.itineary_by_date')}

    + {#if numberOfDays} +

    + {$t('adventures.duration')}: + {numberOfDays} {$t('adventures.days')}

    - - - {/if} - {/each} - + {/if} +

    + Dates: {new Date(collection.start_date).toLocaleDateString(undefined, { + timeZone: 'UTC' + })} - + {new Date(collection.end_date).toLocaleDateString(undefined, { + timeZone: 'UTC' + })} +

    +
    +
    +
    + +
    + {#each Array(numberOfDays) as _, i} + {@const startDate = new Date(collection.start_date)} + {@const tempDate = new Date(startDate.getTime())} + {@const adjustedDate = new Date(tempDate.setUTCDate(tempDate.getUTCDate() + i))} + {@const dateString = adjustedDate.toISOString().split('T')[0]} + + {@const dayAdventures = + groupAdventuresByDate(adventures, new Date(collection.start_date), numberOfDays)[ + dateString + ] || []} + {@const dayTransportations = + groupTransportationsByDate( + transportations, + new Date(collection.start_date), + numberOfDays + )[dateString] || []} + {@const dayNotes = + groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[dateString] || + []} + {@const dayChecklists = + groupChecklistsByDate(checklists, new Date(collection.start_date), numberOfDays)[ + dateString + ] || []} + +
    +
    +

    + {$t('adventures.day')} + {i + 1} +
    + {adjustedDate.toLocaleDateString(undefined, { timeZone: 'UTC' })} +
    +

    + +
    + +
    + {#if dayAdventures.length > 0} + {#each dayAdventures as adventure} + + {/each} + {/if} + {#if dayTransportations.length > 0} + {#each dayTransportations as transportation} + { + transportations = transportations.filter((t) => t.id != event.detail); + }} + on:edit={(event) => { + transportationToEdit = event.detail; + isShowingTransportationModal = true; + }} + /> + {/each} + {/if} + {#if dayNotes.length > 0} + {#each dayNotes as note} + { + noteToEdit = event.detail; + isNoteModalOpen = true; + }} + on:delete={(event) => { + notes = notes.filter((n) => n.id != event.detail); + }} + /> + {/each} + {/if} + {#if dayChecklists.length > 0} + {#each dayChecklists as checklist} + { + notes = notes.filter((n) => n.id != event.detail); + }} + on:edit={(event) => { + checklistToEdit = event.detail; + isShowingChecklistModal = true; + }} + /> + {/each} + {/if} +
    + + {#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0 && dayChecklists.length == 0} +

    {$t('adventures.nothing_planned')}

    + {/if} +
    +
    + {/each} +
    + {/if} + {/if} + + {#if currentView == 'map'} +
    +
    +

    Trip Map

    + + {#each adventures as adventure} + {#if adventure.longitude && adventure.latitude} + + +
    {adventure.name}
    +

    + {adventure.category?.display_name + ' ' + adventure.category?.icon} +

    +
    +
    + {/if} + {/each} + {#each transportations as transportation} + {#if transportation.destination_latitude && transportation.destination_longitude} + + + {getTransportationEmoji(transportation.type)} + + +
    {transportation.name}
    +

    + {transportation.type} +

    +
    +
    + {/if} + {#if transportation.origin_latitude && transportation.origin_longitude} + + + {getTransportationEmoji(transportation.type)} + + +
    {transportation.name}
    +

    + {transportation.type} +

    +
    +
    + {/if} + {/each} +
    +
    +
    + {/if} + {#if currentView == 'calendar'} +
    +
    +

    + {$t('adventures.adventure_calendar')} +

    + +
    +
    {/if} {/if} diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index 45970ab..00139e5 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -57,7 +57,9 @@ {#if recentAdventures.length === 0} -
    +

    {$t('dashboard.no_recent_adventures')}

    {$t('dashboard.add_some')} diff --git a/frontend/src/routes/immich/[key]/+server.ts b/frontend/src/routes/immich/[key]/+server.ts new file mode 100644 index 0000000..33d33de --- /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/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index 1b50518..adb0905 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -106,7 +106,7 @@ function handleSuccessfulLogin(event: RequestEvent, respo path: '/', httpOnly: true, sameSite: 'lax', - secure: true, + secure: event.url.protocol === 'https:', expires: new Date(expiryString) }); } diff --git a/frontend/src/routes/map/+page.svelte b/frontend/src/routes/map/+page.svelte index 705ce1c..f550760 100644 --- a/frontend/src/routes/map/+page.svelte +++ b/frontend/src/routes/map/+page.svelte @@ -3,7 +3,6 @@ import { DefaultMarker, MapEvents, MapLibre, Popup, Marker } from 'svelte-maplibre'; import { t } from 'svelte-i18n'; import type { Adventure, VisitedRegion } from '$lib/types.js'; - import { getAdventureTypeLabel } from '$lib'; import CardCarousel from '$lib/components/CardCarousel.svelte'; import { goto } from '$app/navigation'; export let data; diff --git a/frontend/src/routes/profile/+page.svelte b/frontend/src/routes/profile/+page.svelte index d57df6b..8722c26 100644 --- a/frontend/src/routes/profile/+page.svelte +++ b/frontend/src/routes/profile/+page.svelte @@ -11,82 +11,88 @@ total_countries: number; } | null; - if (data.stats) { - stats = data.stats; - } else { - stats = null; - } - console.log(stats); + stats = data.stats || null; -{#if data.user.profile_pic} -

    -
    - - -
    +
    +
    + + {#if data.user.profile_pic} +
    +
    + Profile +
    +
    + {/if} + + + {#if data.user && data.user.first_name && data.user.last_name} +

    + {data.user.first_name} + {data.user.last_name} +

    + {/if} +

    {data.user.username}

    + + + {#if data.user && data.user.date_joined} +
    +

    {$t('profile.member_since')}

    +
    + +

    + {new Date(data.user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })} +

    +
    +
    + {/if}
    -{/if} -{#if data.user && data.user.first_name && data.user.last_name} -

    - {data.user.first_name} - {data.user.last_name} -

    -{/if} -

    {data.user.username}

    + + {#if stats} +
    -{#if data.user && data.user.date_joined} -

    {$t('profile.member_since')}

    -
    - -

    - {new Date(data.user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })} -

    -
    -{/if} +

    + {$t('profile.user_stats')} +

    -{#if stats} - -
    - -

    {$t('profile.user_stats')}

    - -
    -
    -
    -
    {$t('navbar.adventures')}
    -
    {stats.adventure_count}
    - -
    -
    -
    {$t('navbar.collections')}
    -
    {stats.trips_count}
    - -
    - -
    -
    {$t('profile.visited_countries')}
    -
    - {Math.round((stats.country_count / stats.total_countries) * 100)}% +
    +
    +
    +
    {$t('navbar.adventures')}
    +
    {stats.adventure_count}
    -
    - {stats.country_count}/{stats.total_countries} -
    -
    -
    -
    {$t('profile.visited_regions')}
    -
    - {Math.round((stats.visited_region_count / stats.total_regions) * 100)}% +
    +
    {$t('navbar.collections')}
    +
    {stats.trips_count}
    -
    - {stats.visited_region_count}/{stats.total_regions} + +
    +
    {$t('profile.visited_countries')}
    +
    + {Math.round((stats.country_count / stats.total_countries) * 100)}% +
    +
    + {stats.country_count}/{stats.total_countries} +
    +
    + +
    +
    {$t('profile.visited_regions')}
    +
    + {Math.round((stats.visited_region_count / stats.total_regions) * 100)}% +
    +
    + {stats.visited_region_count}/{stats.total_regions} +
    -
    -{/if} + {/if} +
    Profile | AdventureLog diff --git a/frontend/src/routes/search/+page.svelte b/frontend/src/routes/search/+page.svelte index ac3735e..d85902f 100644 --- a/frontend/src/routes/search/+page.svelte +++ b/frontend/src/routes/search/+page.svelte @@ -110,14 +110,6 @@ id="name" on:change={() => (property = 'name')} /> - (property = 'type')} - /> { 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 2ad898b..6c2b97f 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -2,13 +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) { @@ -18,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 () => { @@ -130,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' @@ -154,195 +213,311 @@ /> {/if} -

    {$t('settings.settings_page')}

    - -

    {$t('settings.account_settings')}

    -
    -
    - -
    - -
    - - -
    - -
    -
    -
    -