Immich Integration
22
README.md
|
@ -83,22 +83,28 @@ Enjoy AdventureLog! 🎉
|
||||||
|
|
||||||
# Screenshots
|
# Screenshots
|
||||||
|
|
||||||

|

|
||||||
Displaying the adventures you have visited and the ones you plan to embark on. You can also filter and sort the adventures.
|
Displays the adventures you have visited and the ones you plan to embark on. You can also filter and sort the adventures.
|
||||||
|
|
||||||

|

|
||||||
Shows specific details about an adventure, including the name, date, location, description, and rating.
|
Shows specific details about an adventure, including the name, date, location, description, and rating.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
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.
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
Displays a summary of your adventures, including your world travel stats.
|
||||||
|
|
||||||

|

|
||||||
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
Lists all the countries you have visited and plan to visit, with the ability to filter by visit status.
|
||||||
|
|
||||||
|

|
||||||
|
Displays the regions for a specific country, includes a map view to visually select regions.
|
||||||
|
|
||||||
# About AdventureLog
|
# About AdventureLog
|
||||||
|
|
||||||
|
|
|
@ -20,4 +20,15 @@ EMAIL_BACKEND='console'
|
||||||
# EMAIL_USE_SSL=True
|
# EMAIL_USE_SSL=True
|
||||||
# EMAIL_HOST_USER='user'
|
# EMAIL_HOST_USER='user'
|
||||||
# EMAIL_HOST_PASSWORD='password'
|
# EMAIL_HOST_PASSWORD='password'
|
||||||
# DEFAULT_FROM_EMAIL='user@example.com'
|
# 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'
|
||||||
|
# ------------------- #
|
|
@ -8,8 +8,6 @@ from allauth.account.decorators import secure_admin_login
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
admin.site.login = secure_admin_login(admin.site.login)
|
admin.site.login = secure_admin_login(admin.site.login)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AdventureAdmin(admin.ModelAdmin):
|
class AdventureAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public')
|
list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public')
|
||||||
list_filter = ( 'user_id', 'is_public')
|
list_filter = ( 'user_id', 'is_public')
|
||||||
|
|
|
@ -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/')),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,7 +1,9 @@
|
||||||
from collections.abc import Collection
|
from collections.abc import Collection
|
||||||
|
import os
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
import uuid
|
import uuid
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.deconstruct import deconstructible
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
@ -257,12 +259,28 @@ class ChecklistItem(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
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):
|
class AdventureImage(models.Model):
|
||||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
user_id = models.ForeignKey(
|
user_id = models.ForeignKey(
|
||||||
User, on_delete=models.CASCADE, default=default_user_id)
|
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)
|
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)
|
||||||
|
is_primary = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.image.url
|
return self.image.url
|
||||||
|
|
|
@ -8,7 +8,7 @@ from main.utils import CustomModelSerializer
|
||||||
class AdventureImageSerializer(CustomModelSerializer):
|
class AdventureImageSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdventureImage
|
model = AdventureImage
|
||||||
fields = ['id', 'image', 'adventure']
|
fields = ['id', 'image', 'adventure', 'is_primary']
|
||||||
read_only_fields = ['id']
|
read_only_fields = ['id']
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
|
@ -116,7 +116,7 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def create(self, validated_data):
|
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)
|
category_data = validated_data.pop('category', None)
|
||||||
print(category_data)
|
print(category_data)
|
||||||
adventure = Adventure.objects.create(**validated_data)
|
adventure = Adventure.objects.create(**validated_data)
|
||||||
|
@ -131,6 +131,7 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
return adventure
|
return adventure
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
|
has_visits = 'visits' in validated_data
|
||||||
visits_data = validated_data.pop('visits', [])
|
visits_data = validated_data.pop('visits', [])
|
||||||
category_data = validated_data.pop('category', None)
|
category_data = validated_data.pop('category', None)
|
||||||
|
|
||||||
|
@ -142,24 +143,25 @@ class AdventureSerializer(CustomModelSerializer):
|
||||||
instance.category = category
|
instance.category = category
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
current_visits = instance.visits.all()
|
if has_visits:
|
||||||
current_visit_ids = set(current_visits.values_list('id', flat=True))
|
current_visits = instance.visits.all()
|
||||||
|
current_visit_ids = set(current_visits.values_list('id', flat=True))
|
||||||
|
|
||||||
updated_visit_ids = set()
|
updated_visit_ids = set()
|
||||||
for visit_data in visits_data:
|
for visit_data in visits_data:
|
||||||
visit_id = visit_data.get('id')
|
visit_id = visit_data.get('id')
|
||||||
if visit_id and visit_id in current_visit_ids:
|
if visit_id and visit_id in current_visit_ids:
|
||||||
visit = current_visits.get(id=visit_id)
|
visit = current_visits.get(id=visit_id)
|
||||||
for attr, value in visit_data.items():
|
for attr, value in visit_data.items():
|
||||||
setattr(visit, attr, value)
|
setattr(visit, attr, value)
|
||||||
visit.save()
|
visit.save()
|
||||||
updated_visit_ids.add(visit_id)
|
updated_visit_ids.add(visit_id)
|
||||||
else:
|
else:
|
||||||
new_visit = Visit.objects.create(adventure=instance, **visit_data)
|
new_visit = Visit.objects.create(adventure=instance, **visit_data)
|
||||||
updated_visit_ids.add(new_visit.id)
|
updated_visit_ids.add(new_visit.id)
|
||||||
|
|
||||||
visits_to_delete = current_visit_ids - updated_visit_ids
|
visits_to_delete = current_visit_ids - updated_visit_ids
|
||||||
instance.visits.filter(id__in=visits_to_delete).delete()
|
instance.visits.filter(id__in=visits_to_delete).delete()
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
|
@ -1032,7 +1032,29 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def image_delete(self, request, *args, **kwargs):
|
def image_delete(self, request, *args, **kwargs):
|
||||||
return self.destroy(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):
|
def create(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
|
|
0
backend/server/integrations/__init__.py
Normal file
9
backend/server/integrations/admin.py
Normal file
|
@ -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)
|
6
backend/server/integrations/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'integrations'
|
27
backend/server/integrations/migrations/0001_initial.py
Normal file
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
backend/server/integrations/migrations/__init__.py
Normal file
15
backend/server/integrations/models.py
Normal file
|
@ -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
|
13
backend/server/integrations/serializers.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from .models import ImmichIntegration
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
class ImmichIntegrationSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ImmichIntegration
|
||||||
|
fields = '__all__'
|
||||||
|
read_only_fields = ['id', 'user']
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
representation = super().to_representation(instance)
|
||||||
|
representation.pop('user', None)
|
||||||
|
return representation
|
3
backend/server/integrations/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
14
backend/server/integrations/urls.py
Normal file
|
@ -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
|
||||||
|
]
|
314
backend/server/integrations/views.py
Normal file
|
@ -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<imageid>[^/.]+)')
|
||||||
|
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<albumid>[^/.]+)')
|
||||||
|
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
|
||||||
|
)
|
|
@ -56,6 +56,7 @@ INSTALLED_APPS = (
|
||||||
'adventures',
|
'adventures',
|
||||||
'worldtravel',
|
'worldtravel',
|
||||||
'users',
|
'users',
|
||||||
|
'integrations',
|
||||||
'django.contrib.gis',
|
'django.contrib.gis',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -164,9 +165,6 @@ TEMPLATES = [
|
||||||
DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True'
|
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.')
|
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'
|
AUTH_USER_MODEL = 'users.CustomUser'
|
||||||
|
|
||||||
ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter'
|
ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter'
|
||||||
|
@ -222,10 +220,16 @@ REST_FRAMEWORK = {
|
||||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
||||||
}
|
}
|
||||||
|
|
||||||
SWAGGER_SETTINGS = {
|
if DEBUG:
|
||||||
'LOGIN_URL': 'login',
|
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = (
|
||||||
'LOGOUT_URL': 'logout',
|
'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()]
|
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,8 @@ urlpatterns = [
|
||||||
# path('auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent'),
|
# path('auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent'),
|
||||||
path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
|
|
||||||
|
path("api/integrations/", include("integrations.urls")),
|
||||||
|
|
||||||
# Include the API endpoints:
|
# Include the API endpoints:
|
||||||
|
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
>Documentation</a
|
>Documentation</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
>Source Code</a
|
>Source Code</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
<li><a href="/docs">API Docs</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!--/.nav-collapse -->
|
<!--/.nav-collapse -->
|
||||||
|
|
|
@ -68,6 +68,8 @@ class Command(BaseCommand):
|
||||||
country_name = country['name']
|
country_name = country['name']
|
||||||
country_subregion = country['subregion']
|
country_subregion = country['subregion']
|
||||||
country_capital = country['capital']
|
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)
|
processed_country_codes.add(country_code)
|
||||||
|
|
||||||
|
@ -76,13 +78,17 @@ class Command(BaseCommand):
|
||||||
country_obj.name = country_name
|
country_obj.name = country_name
|
||||||
country_obj.subregion = country_subregion
|
country_obj.subregion = country_subregion
|
||||||
country_obj.capital = country_capital
|
country_obj.capital = country_capital
|
||||||
|
country_obj.longitude = longitude
|
||||||
|
country_obj.latitude = latitude
|
||||||
countries_to_update.append(country_obj)
|
countries_to_update.append(country_obj)
|
||||||
else:
|
else:
|
||||||
country_obj = Country(
|
country_obj = Country(
|
||||||
name=country_name,
|
name=country_name,
|
||||||
country_code=country_code,
|
country_code=country_code,
|
||||||
subregion=country_subregion,
|
subregion=country_subregion,
|
||||||
capital=country_capital
|
capital=country_capital,
|
||||||
|
longitude=longitude,
|
||||||
|
latitude=latitude
|
||||||
)
|
)
|
||||||
countries_to_create.append(country_obj)
|
countries_to_create.append(country_obj)
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -15,6 +15,8 @@ class Country(models.Model):
|
||||||
country_code = models.CharField(max_length=2, unique=True) #iso2 code
|
country_code = models.CharField(max_length=2, unique=True) #iso2 code
|
||||||
subregion = models.CharField(max_length=100, blank=True, null=True)
|
subregion = models.CharField(max_length=100, blank=True, null=True)
|
||||||
capital = 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:
|
class Meta:
|
||||||
verbose_name = "Country"
|
verbose_name = "Country"
|
||||||
|
|
|
@ -29,7 +29,7 @@ class CountrySerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Country
|
model = Country
|
||||||
fields = '__all__'
|
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):
|
class RegionSerializer(serializers.ModelSerializer):
|
||||||
|
|
BIN
brand/adventurelog.png
Normal file
After Width: | Height: | Size: 87 KiB |
313
brand/adventurelog.svg
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="2000"
|
||||||
|
height="2000"
|
||||||
|
viewBox="0 0 529.16665 529.16666"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="AdventureLog.svg"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||||
|
inkscape:export-filename="AdventureLog.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.35190083"
|
||||||
|
inkscape:cx="1140.9464"
|
||||||
|
inkscape:cy="1112.5293"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1131"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter389"
|
||||||
|
x="-1.0282219"
|
||||||
|
y="-0.73747355"
|
||||||
|
width="3.0639595"
|
||||||
|
height="2.4803376">
|
||||||
|
<feFlood
|
||||||
|
result="flood"
|
||||||
|
in="SourceGraphic"
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
id="feFlood388" />
|
||||||
|
<feGaussianBlur
|
||||||
|
result="blur"
|
||||||
|
in="SourceGraphic"
|
||||||
|
stdDeviation="57.004227"
|
||||||
|
id="feGaussianBlur388" />
|
||||||
|
<feOffset
|
||||||
|
result="offset"
|
||||||
|
in="blur"
|
||||||
|
dx="1.000000"
|
||||||
|
dy="1.000000"
|
||||||
|
id="feOffset388" />
|
||||||
|
<feComposite
|
||||||
|
result="comp1"
|
||||||
|
operator="in"
|
||||||
|
in="flood"
|
||||||
|
in2="offset"
|
||||||
|
id="feComposite388" />
|
||||||
|
<feComposite
|
||||||
|
result="comp2"
|
||||||
|
operator="over"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="comp1"
|
||||||
|
id="feComposite389" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter391"
|
||||||
|
x="-0.59415994"
|
||||||
|
y="-1.3323052"
|
||||||
|
width="2.1926628"
|
||||||
|
height="3.6743487">
|
||||||
|
<feFlood
|
||||||
|
result="flood"
|
||||||
|
in="SourceGraphic"
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
id="feFlood389" />
|
||||||
|
<feGaussianBlur
|
||||||
|
result="blur"
|
||||||
|
in="SourceGraphic"
|
||||||
|
stdDeviation="57.004227"
|
||||||
|
id="feGaussianBlur389" />
|
||||||
|
<feOffset
|
||||||
|
result="offset"
|
||||||
|
in="blur"
|
||||||
|
dx="1.000000"
|
||||||
|
dy="1.000000"
|
||||||
|
id="feOffset389" />
|
||||||
|
<feComposite
|
||||||
|
result="comp1"
|
||||||
|
operator="in"
|
||||||
|
in="flood"
|
||||||
|
in2="offset"
|
||||||
|
id="feComposite390" />
|
||||||
|
<feComposite
|
||||||
|
result="comp2"
|
||||||
|
operator="over"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="comp1"
|
||||||
|
id="feComposite391" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter393"
|
||||||
|
x="-1.1482379"
|
||||||
|
y="-0.96121423"
|
||||||
|
width="3.3048687"
|
||||||
|
height="2.9294544">
|
||||||
|
<feFlood
|
||||||
|
result="flood"
|
||||||
|
in="SourceGraphic"
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
id="feFlood391" />
|
||||||
|
<feGaussianBlur
|
||||||
|
result="blur"
|
||||||
|
in="SourceGraphic"
|
||||||
|
stdDeviation="57.004227"
|
||||||
|
id="feGaussianBlur391" />
|
||||||
|
<feOffset
|
||||||
|
result="offset"
|
||||||
|
in="blur"
|
||||||
|
dx="1.000000"
|
||||||
|
dy="1.000000"
|
||||||
|
id="feOffset391" />
|
||||||
|
<feComposite
|
||||||
|
result="comp1"
|
||||||
|
operator="in"
|
||||||
|
in="flood"
|
||||||
|
in2="offset"
|
||||||
|
id="feComposite392" />
|
||||||
|
<feComposite
|
||||||
|
result="comp2"
|
||||||
|
operator="over"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="comp1"
|
||||||
|
id="feComposite393" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter395"
|
||||||
|
x="-1.3398814"
|
||||||
|
y="-1.1275613"
|
||||||
|
width="3.6895566"
|
||||||
|
height="3.2633644">
|
||||||
|
<feFlood
|
||||||
|
result="flood"
|
||||||
|
in="SourceGraphic"
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
id="feFlood393" />
|
||||||
|
<feGaussianBlur
|
||||||
|
result="blur"
|
||||||
|
in="SourceGraphic"
|
||||||
|
stdDeviation="57.004227"
|
||||||
|
id="feGaussianBlur393" />
|
||||||
|
<feOffset
|
||||||
|
result="offset"
|
||||||
|
in="blur"
|
||||||
|
dx="1.000000"
|
||||||
|
dy="1.000000"
|
||||||
|
id="feOffset393" />
|
||||||
|
<feComposite
|
||||||
|
result="comp1"
|
||||||
|
operator="in"
|
||||||
|
in="flood"
|
||||||
|
in2="offset"
|
||||||
|
id="feComposite394" />
|
||||||
|
<feComposite
|
||||||
|
result="comp2"
|
||||||
|
operator="over"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="comp1"
|
||||||
|
id="feComposite395" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter397"
|
||||||
|
x="-1.8571666"
|
||||||
|
y="-0.84804253"
|
||||||
|
width="4.7279079"
|
||||||
|
height="2.7022837">
|
||||||
|
<feFlood
|
||||||
|
result="flood"
|
||||||
|
in="SourceGraphic"
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
id="feFlood395" />
|
||||||
|
<feGaussianBlur
|
||||||
|
result="blur"
|
||||||
|
in="SourceGraphic"
|
||||||
|
stdDeviation="57.004227"
|
||||||
|
id="feGaussianBlur395" />
|
||||||
|
<feOffset
|
||||||
|
result="offset"
|
||||||
|
in="blur"
|
||||||
|
dx="1.000000"
|
||||||
|
dy="1.000000"
|
||||||
|
id="feOffset395" />
|
||||||
|
<feComposite
|
||||||
|
result="comp1"
|
||||||
|
operator="in"
|
||||||
|
in="flood"
|
||||||
|
in2="offset"
|
||||||
|
id="feComposite396" />
|
||||||
|
<feComposite
|
||||||
|
result="comp2"
|
||||||
|
operator="over"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="comp1"
|
||||||
|
id="feComposite397" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB;"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter427"
|
||||||
|
x="-0.096054554"
|
||||||
|
y="-0.10772674"
|
||||||
|
width="1.1947073"
|
||||||
|
height="1.2117496">
|
||||||
|
<feFlood
|
||||||
|
result="flood"
|
||||||
|
in="SourceGraphic"
|
||||||
|
flood-opacity="0.498039"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
id="feFlood426" />
|
||||||
|
<feGaussianBlur
|
||||||
|
result="blur"
|
||||||
|
in="SourceGraphic"
|
||||||
|
stdDeviation="13.320559"
|
||||||
|
id="feGaussianBlur426" />
|
||||||
|
<feOffset
|
||||||
|
result="offset"
|
||||||
|
in="blur"
|
||||||
|
dx="1.000000"
|
||||||
|
dy="1.000000"
|
||||||
|
id="feOffset426" />
|
||||||
|
<feComposite
|
||||||
|
result="comp1"
|
||||||
|
operator="in"
|
||||||
|
in="flood"
|
||||||
|
in2="offset"
|
||||||
|
id="feComposite426" />
|
||||||
|
<feComposite
|
||||||
|
result="comp2"
|
||||||
|
operator="over"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="comp1"
|
||||||
|
id="feComposite427" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g379">
|
||||||
|
<path
|
||||||
|
style="display:inline;fill:#48b5ff;fill-opacity:1;stroke-width:94.45;stroke-dasharray:none"
|
||||||
|
d="m 971.77794,1568.1491 -215.24252,-108.5775 -232.07676,98.4805 -232.07678,98.4806 -0.63034,-550.1231 c -0.34668,-302.56774 0.21559,-550.94143 1.2495,-551.94157 1.03391,-1.00014 105.33804,-45.69109 231.78696,-99.31323 L 754.69512,357.65999 970.68644,466.2139 c 118.79516,59.70465 217.23796,108.5539 218.76156,108.5539 1.5236,0 108.4326,-50.70974 237.5755,-112.68831 129.1428,-61.97858 245.2097,-117.568 257.9264,-123.53205 l 23.1212,-10.84372 -0.6303,551.00102 -0.6303,551.00106 -257.396,123.4976 c -141.5678,67.9237 -258.5206,123.5034 -259.895,123.5104 -1.3745,0.01 -99.3582,-48.8471 -217.74156,-108.5647 z"
|
||||||
|
id="path1"
|
||||||
|
transform="scale(0.26458333)" />
|
||||||
|
<path
|
||||||
|
style="fill:#00ff00;fill-opacity:1;stroke-width:10;stroke-dasharray:none;filter:url(#filter389)"
|
||||||
|
d="m 154.50783,108.88503 c -2.85572,6.51915 -3.99705,17.36183 -0.2277,23.7036 3.00168,5.05017 8.28922,6.62967 13.3295,9.04742 3.90851,1.87485 7.96149,3.93177 11.47307,6.51256 13.62566,10.01398 23.98335,27.67115 20.06743,44.94435 -0.12449,0.54916 -1.48104,7.01695 -1.85641,7.87642 -2.34196,5.36214 -12.56252,15.09064 -18.05999,17.60459 -0.31647,0.14472 -6.14257,1.6119 -6.77744,1.77975 -5.74767,1.51955 -11.84,3.00805 -16.77513,6.51256 -2.81536,1.99923 -13.27557,11.47452 -14.84205,14.54 -0.76687,1.5007 -1.22537,3.14442 -1.97026,4.65615 -2.34545,4.75997 -5.79169,9.60118 -9.20077,13.63154 -3.11382,3.68129 -2.36218,2.17313 -5.86897,5.3764 -6.0653,5.54035 -12.38233,10.68303 -18.66873,15.97822 -2.95625,2.4901 -1.77292,2.02049 -4.80717,4.24024 -4.376145,3.20143 -19.485134,11.83259 -25.104617,8.25513 -5.798267,-3.69128 -1.637855,-18.91136 -2.537182,-24.27052 -0.665342,-3.96483 -2.868842,-7.73278 -3.824359,-11.66126 -1.060926,-4.36186 0.244798,-8.61424 0.415894,-12.95078 0.166198,-4.2124 0.437509,-8.63608 -0.03717,-12.8346 -0.54496,-4.82011 -2.197963,-8.2219 -2.197963,-13.32717 0,-3.83658 -0.26317,-7.9553 0.0395,-11.77513 0.113016,-1.42634 0.682535,-2.78477 0.871283,-4.20307 0.705311,-5.2999 1.237988,-11.08737 0.831787,-16.4336 -0.205095,-2.69936 5.511498,-10.74899 5.093496,-13.38624 -0.980816,-6.18814 -7.14978,-6.25695 -6.304002,-12.32247 0.451585,-3.23855 0.187248,-7.10749 1.740246,-10.07205 0.835928,-1.59571 1.639732,-4.10023 2.915902,-5.3764 3.741116,-3.74112 13.330719,-6.06402 18.250511,-7.60923 3.127833,-0.98238 6.027592,-2.45779 8.975394,-3.86385 3.27336,-1.56136 5.87838,-3.71819 8.93589,-5.60178 3.52017,-2.16861 7.75174,-3.29655 11.51025,-4.96052 11.45567,-5.07163 22.44821,-10.89093 34.60976,-14.01026 z"
|
||||||
|
id="path2"
|
||||||
|
sodipodi:nodetypes="csssscssssssssssssssssssssssssssc" />
|
||||||
|
<path
|
||||||
|
style="fill:#00ff00;fill-opacity:1;stroke-width:10;stroke-dasharray:none;filter:url(#filter393)"
|
||||||
|
d="m 282.71911,236.75014 c -1.07341,-0.51813 -2.0389,-1.39597 -3.22027,-1.55438 -1.88367,-0.25258 -5.13392,0.85934 -7.00513,1.44053 -8.45275,2.62538 -18.44379,6.81757 -22.49075,15.37179 -3.20748,6.77976 -1.80841,13.94405 -1.21283,21.05255 0.70345,8.39597 0.60913,17.64626 3.06924,25.78307 3.80766,12.59377 15.78781,28.09023 29.11717,31.23845 5.76255,1.36104 8.68662,1.0038 15.10925,0.26487 11.5788,-1.33212 23.20626,-7.9298 31.04795,-16.39408 3.10414,-3.3506 5.50955,-7.21715 8.59666,-10.6018 3.18743,-3.49465 5.51775,-7.04064 8.06463,-10.9805 3.48445,-5.39025 5.91,-9.43047 3.44564,-16.12924 -1.0297,-2.79895 -5.0392,-5.98461 -7.08181,-8.10411 -4.91808,-5.10316 -8.81666,-9.96675 -9.42845,-17.30255 -0.51679,-6.19651 0.806,-12.46011 0.11382,-18.62923 -0.87048,-7.75843 -3.35968,-15.22014 -5.56458,-22.67895 -1.97014,-6.66463 -5.2514,-14.24288 -11.70078,-17.79745 -15.70897,-8.65796 -36.07811,2.92981 -49.03591,11.73795 -1.87759,1.2763 -4.03614,1.97474 -5.86898,3.29462 -1.50247,1.08197 -2.65518,2.55672 -4.05205,3.74768 -2.7825,2.37234 -5.73488,4.72293 -8.59435,7.00513 -6.38056,5.09245 -15.28401,9.78925 -16.88899,18.59206 -0.67926,3.72553 7.14966,3.49307 9.04975,3.44332 9.16411,-0.23998 18.38306,-4.78561 26.08975,-9.42615 2.57984,-1.55343 5.60029,-3.28025 8.59434,-3.90103 3.15601,-0.65434 6.73357,-0.98782 9.69333,0.56924 1.40962,0.74156 2.32511,2.61628 3.3713,3.74769 3.81595,4.12676 4.11615,7.5098 -3.21795,6.21052 z"
|
||||||
|
id="path5" />
|
||||||
|
<path
|
||||||
|
style="fill:#00ff00;fill-opacity:1;stroke-width:10;stroke-dasharray:none;filter:url(#filter391)"
|
||||||
|
d="m 99.110381,433.18186 c 4.670059,-2.86644 7.566889,-7.59439 11.398729,-11.3964 11.22457,-11.13721 20.23699,-24.24948 28.43641,-37.74871 5.53049,-9.10519 9.71389,-19.38771 16.16872,-27.90433 3.11752,-4.11332 7.50709,-7.12695 11.43358,-10.41361 4.20791,-3.52221 7.6504,-6.81593 12.8741,-8.67103 15.36185,-5.45544 26.73636,1.95538 38.47129,11.2454 3.5267,2.79191 7.05706,4.28564 10.90616,6.47539 4.29758,2.44485 7.73021,6.21292 12.19102,8.44333 8.94937,4.47469 19.38222,5.65478 29.15668,6.89126 7.14631,0.90405 14.16066,2.50237 21.1664,4.12641 16.46849,3.81768 33.64484,8.74959 32.67668,29.34489 -0.28171,5.99241 -3.32624,12.60742 -8.02513,16.39408 -3.91306,3.15339 -9.22134,3.33169 -13.89873,4.20307 -5.87557,1.09461 -11.90458,2.75058 -17.94615,2.91592 -3.19683,0.0875 -11.4417,-2.50979 -14.9954,-3.33179 -3.80158,-0.87937 -8.26721,-0.9415 -11.73793,-2.84158 -3.87055,-2.11894 -6.90769,-5.47743 -10.45078,-8.0251 -4.87127,-3.50271 -1.08518,-0.58992 -4.96051,-2.91589 -3.30897,-1.98607 -6.204,-4.669 -9.57948,-6.54974 -5.1211,-2.8534 -13.86293,-3.58071 -19.69104,-4.77231 -5.67771,-1.16089 -11.01578,-3.30923 -16.81231,-4.01257 -13.91552,-1.68849 -29.45142,5.70987 -40.9318,13.09947 -2.56659,1.65206 -4.97173,3.56039 -7.42102,5.33924 -2.67583,1.94339 -5.80257,3.32094 -8.7082,4.88384 -7.53479,4.05288 -15.4307,7.2287 -22.90898,11.35922 -2.00201,1.1058 -11.46055,6.02861 -13.17615,5.68079 -1.32827,-0.26929 -2.33944,-2.21337 -3.636159,-1.81925 -2.267678,0.68921 -3.219347,3.63569 -5.339231,4.69564"
|
||||||
|
id="path6" />
|
||||||
|
<path
|
||||||
|
style="fill:#00ff00;fill-opacity:1;stroke-width:10;stroke-dasharray:none;filter:url(#filter395)"
|
||||||
|
d="m 450.19631,298.04907 c -5.5282,0.50496 -11.31189,-0.22132 -16.58461,1.51487 -12.17369,4.0086 -28.70549,15.28393 -34.1172,28.28309 -2.07438,4.98277 -2.95732,10.25334 -3.37129,15.59946 -0.22418,2.89552 -0.0933,5.87015 -0.83177,8.70821 -1.64349,6.31634 -4.7022,13.0625 -8.78488,18.17616 -2.91534,3.65154 -6.67846,6.51549 -10.14873,9.54 -8.24569,7.18651 -23.60925,23.91071 -21.96103,36.31049 0.19262,1.44907 0.77642,2.27965 2.1213,2.87872 2.17652,0.96954 6.3614,-0.53234 8.63153,-0.8341 7.76113,-1.03164 12.12755,-1.31003 19.57718,-5.03486 1.44111,-0.72054 2.84964,-1.3653 4.31694,-2.04462 6.05637,-2.80398 11.89083,-6.01507 17.83461,-9.04973 2.26536,-1.15663 4.74779,-1.77562 7.04231,-2.87642 2.15358,-1.03317 3.83749,-2.63954 5.98281,-3.67334 1.5544,-0.74904 3.25289,-1.02836 4.80949,-1.70307 1.86055,-0.80645 3.54978,-1.97313 5.33924,-2.87872 2.17898,-1.10271 4.61735,-1.2749 6.92846,-1.8936 1.4836,-0.39716 2.68676,-1.23536 4.08921,-1.81692 1.65156,-0.68485 3.50653,-0.57332 5.22539,-0.98512 1.56427,-0.37476 2.48695,-2.11201 3.74769,-2.99024 0.6309,-0.4395 1.52495,-0.5375 2.00745,-1.13618 0.48395,-0.60047 0.25164,-1.54802 0.6064,-2.23279 0.46074,-0.88932 1.51323,-1.21002 1.96794,-2.1213 1.8632,-3.73398 0.31491,-12.51823 0.41823,-16.62178 0.11186,-4.44304 0.41844,-8.86217 0.71795,-13.29 0.23315,-3.44704 -0.22538,-6.93523 -0.22538,-10.3741 0,-1.49648 0.38465,-2.89922 0.30203,-4.39359 -0.0821,-1.48571 -0.45538,-2.97958 -0.45538,-4.46796 0,-3.04234 0.0308,0.34052 0.49258,-2.53484 0.34938,-2.17554 0.005,-4.54488 0.0767,-6.74026 0.0808,-2.47037 0.58761,-4.89522 0.37872,-7.38386 -0.13973,-1.66495 -1.12795,-2.77178 -1.32667,-4.39127 -0.18376,-1.49751 0.63254,-5.63655 0,-6.74026 -0.3973,-0.69326 -1.71445,-0.36851 -2.23282,-0.72027 -0.91319,-0.61968 -1.71622,-1.38785 -2.57435,-2.0818 z"
|
||||||
|
id="path7" />
|
||||||
|
<path
|
||||||
|
style="fill:#00ff00;fill-opacity:1;stroke-width:10;stroke-dasharray:none;filter:url(#filter397)"
|
||||||
|
d="m 375.33553,121.34324 c 3.39913,22.93503 -2.23867,43.81133 -8.17846,65.50203 -3.10168,11.32658 -4.27915,22.46486 -4.96051,34.11486 -0.32861,5.61878 -0.89162,6.02837 -0.26487,12.41872 0.34464,3.51408 1.85081,7.80185 3.29461,11.01768 1.13398,2.52573 4.32978,4.06396 6.85411,4.73282 14.37217,3.80815 26.65789,-2.23088 33.69898,-15.18127 6.74126,-12.399 4.57229,-24.42084 3.86151,-37.75102 -0.38232,-7.17036 -0.76689,-14.97137 -0.26487,-22.11205 0.6106,-8.68483 5.02068,-16.55987 8.71053,-24.231 2.27978,-4.73962 3.62913,-9.80406 5.52744,-14.69103 1.30437,-3.35796 2.65044,-5.86766 3.82436,-9.39129 1.51609,-4.55069 0.62532,-9.15948 1.17333,-13.78023 0.47889,-4.03804 2.7718,-7.5475 3.82436,-11.39873 1.04624,-3.828179 1.90934,-7.787484 2.87872,-11.661277"
|
||||||
|
id="path8" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:1;stroke:#afafaf;stroke-width:10;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter427)"
|
||||||
|
d="M 456.91785,381.08869 314.6716,449.29929 199.88458,391.55258 72.03927,445.77735 72.039268,143.494 199.88458,89.269234 314.6716,147.01594 456.91785,78.805342 Z"
|
||||||
|
id="path2-2"
|
||||||
|
sodipodi:nodetypes="ccccccccc" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
id="rect378"
|
||||||
|
style="fill:#6d6d6d;fill-opacity:0.31908;stroke-width:16.7412"
|
||||||
|
d="m 200.16234,83.744919 114.47572,57.762111 0,313.26052 -114.47572,-57.8148 z"
|
||||||
|
sodipodi:nodetypes="ccccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 16 KiB |
BIN
brand/banner.png
Normal file
After Width: | Height: | Size: 882 KiB |
BIN
brand/screenshots/adventures.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
brand/screenshots/countries.png
Normal file
After Width: | Height: | Size: 214 KiB |
BIN
brand/screenshots/dashboard.png
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
brand/screenshots/details.png
Normal file
After Width: | Height: | Size: 848 KiB |
BIN
brand/screenshots/edit.png
Normal file
After Width: | Height: | Size: 656 KiB |
BIN
brand/screenshots/itinerary.png
Normal file
After Width: | Height: | Size: 735 KiB |
BIN
brand/screenshots/map.png
Normal file
After Width: | Height: | Size: 478 KiB |
BIN
brand/screenshots/regions.png
Normal file
After Width: | Height: | Size: 159 KiB |
|
@ -87,6 +87,10 @@ export default defineConfig({
|
||||||
text: "Configuration",
|
text: "Configuration",
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
text: "Immich Integration",
|
||||||
|
link: "/docs/configuration/immich_integration",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: "Update App",
|
text: "Update App",
|
||||||
link: "/docs/configuration/updating",
|
link: "/docs/configuration/updating",
|
||||||
|
@ -131,6 +135,10 @@ export default defineConfig({
|
||||||
text: "Changelogs",
|
text: "Changelogs",
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
text: "v0.8.0",
|
||||||
|
link: "/docs/changelogs/v0-8-0",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: "v0.7.1",
|
text: "v0.7.1",
|
||||||
link: "/docs/changelogs/v0-7-1",
|
link: "/docs/changelogs/v0-7-1",
|
||||||
|
|
105
documentation/docs/changelogs/v0-8-0.md
Normal file
|
@ -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 💖
|
||||||
|
|
||||||
|
[](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)
|
|
@ -22,3 +22,13 @@ environment:
|
||||||
- EMAIL_HOST_PASSWORD='password'
|
- EMAIL_HOST_PASSWORD='password'
|
||||||
- DEFAULT_FROM_EMAIL='user@example.com'
|
- 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`
|
||||||
|
|
28
documentation/docs/configuration/immich_integration.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Immich Integration
|
||||||
|
|
||||||
|
### What is Immich?
|
||||||
|
|
||||||
|
<!-- immich banner -->
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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! 🎉
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "adventurelog-frontend",
|
"name": "adventurelog-frontend",
|
||||||
"version": "0.7.1",
|
"version": "0.8.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"django": "cd .. && cd backend/server && python3 manage.py runserver",
|
"django": "cd .. && cd backend/server && python3 manage.py runserver",
|
||||||
|
|
1
frontend/src/lib/assets/immich.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#fa2921" d="M22.384 14.575c3.143 2.782 5.675 5.764 7.305 8.574 2.8-5.007 4.67-10.957 4.694-14.746V8.33c0-5.607-5.593-7.79-10.411-7.79S13.56 2.723 13.56 8.33v.303c2.686 1.194 5.87 3.327 8.824 5.943"/><path fill="#ed79b5" d="M5.24 29.865c1.965-2.185 4.978-4.554 8.379-6.556 3.618-2.13 7.236-3.617 10.412-4.298-3.897-4.21-8.977-7.827-12.574-9.02l-.07-.023C6.054 8.236 2.25 12.88.762 17.463S-.38 28.039 4.953 29.77z"/><path fill="#ffb400" d="M47.238 17.385c-1.488-4.582-5.292-9.227-10.625-7.494l-.288.093c-.305 2.922-1.35 6.61-2.925 10.229-1.674 3.848-3.728 7.179-5.897 9.597 5.627 1.116 11.863 1.055 15.474-.093l.07-.023c5.333-1.733 5.68-7.727 4.191-12.309"/><path fill="#1e83f7" d="M19.217 34.345c-.907-4.099-1.204-8-.87-11.23-5.208 2.404-10.218 6.118-12.465 9.17l-.043.06c-3.296 4.537-.054 9.59 3.844 12.42 3.898 2.833 9.706 4.355 13.002-.181l.178-.245c-1.471-2.543-2.794-6.142-3.646-9.994"/><path fill="#18c249" d="M42.074 32.052c-2.874.613-6.704.759-10.632.379-4.178-.403-7.98-1.327-10.95-2.643.678 5.695 2.662 11.608 4.87 14.688l.044.06c3.295 4.536 9.103 3.014 13.001.182s7.14-7.885 3.845-12.421z"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -191,7 +191,7 @@
|
||||||
<!-- action options dropdown -->
|
<!-- action options dropdown -->
|
||||||
|
|
||||||
{#if type != 'link'}
|
{#if type != 'link'}
|
||||||
{#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))}
|
{#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with?.includes(user.uuid))}
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-neutral-200">
|
<div tabindex="0" role="button" class="btn btn-neutral-200">
|
||||||
<DotsHorizontal class="w-6 h-6" />
|
<DotsHorizontal class="w-6 h-6" />
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
import { deserialize } from '$app/forms';
|
import { deserialize } from '$app/forms';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let longitude: number | null = null;
|
export let longitude: number | null = null;
|
||||||
export let latitude: number | null = null;
|
export let latitude: number | null = null;
|
||||||
export let collection: Collection | null = null;
|
export let collection: Collection | null = null;
|
||||||
|
@ -22,7 +21,7 @@
|
||||||
|
|
||||||
let query: string = '';
|
let query: string = '';
|
||||||
let places: OpenStreetMapPlace[] = [];
|
let places: OpenStreetMapPlace[] = [];
|
||||||
let images: { id: string; image: string }[] = [];
|
let images: { id: string; image: string; is_primary: boolean }[] = [];
|
||||||
let warningMessage: string = '';
|
let warningMessage: string = '';
|
||||||
let constrainDates: boolean = false;
|
let constrainDates: boolean = false;
|
||||||
|
|
||||||
|
@ -33,6 +32,10 @@
|
||||||
import CategoryDropdown from './CategoryDropdown.svelte';
|
import CategoryDropdown from './CategoryDropdown.svelte';
|
||||||
import { findFirstValue } from '$lib';
|
import { findFirstValue } from '$lib';
|
||||||
import MarkdownEditor from './MarkdownEditor.svelte';
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
||||||
|
import ImmichSelect from './ImmichSelect.svelte';
|
||||||
|
|
||||||
|
import Star from '~icons/mdi/star';
|
||||||
|
import Crown from '~icons/mdi/crown';
|
||||||
|
|
||||||
let wikiError: string = '';
|
let wikiError: string = '';
|
||||||
|
|
||||||
|
@ -161,6 +164,33 @@
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let willBeMarkedVisited: boolean = false;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
willBeMarkedVisited = false; // Reset before evaluating
|
||||||
|
|
||||||
|
const today = new Date(); // Cache today's date to avoid redundant calculations
|
||||||
|
|
||||||
|
for (const visit of adventure.visits) {
|
||||||
|
const startDate = new Date(visit.start_date);
|
||||||
|
const endDate = visit.end_date ? new Date(visit.end_date) : null;
|
||||||
|
|
||||||
|
// If the visit has both a start date and an end date, check if it started by today
|
||||||
|
if (startDate && endDate && startDate <= today) {
|
||||||
|
willBeMarkedVisited = true;
|
||||||
|
break; // Exit the loop since we've determined the result
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the visit has a start date but no end date, check if it started by today
|
||||||
|
if (startDate && !endDate && startDate <= today) {
|
||||||
|
willBeMarkedVisited = true;
|
||||||
|
break; // Exit the loop since we've determined the result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('WMBV:', willBeMarkedVisited);
|
||||||
|
}
|
||||||
|
|
||||||
let previousCoords: { lat: number; lng: number } | null = null;
|
let previousCoords: { lat: number; lng: number } | null = null;
|
||||||
|
|
||||||
$: if (markers.length > 0) {
|
$: if (markers.length > 0) {
|
||||||
|
@ -179,28 +209,70 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchImage() {
|
async function makePrimaryImage(image_id: string) {
|
||||||
let res = await fetch(url);
|
let res = await fetch(`/api/images/${image_id}/toggle_primary`, {
|
||||||
let data = await res.blob();
|
method: 'POST'
|
||||||
if (!data) {
|
|
||||||
imageError = $t('adventures.no_image_url');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let file = new File([data], 'image.jpg', { type: 'image/jpeg' });
|
|
||||||
let formData = new FormData();
|
|
||||||
formData.append('image', file);
|
|
||||||
formData.append('adventure', adventure.id);
|
|
||||||
let res2 = await fetch(`/adventures?/image`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
});
|
||||||
let data2 = await res2.json();
|
if (res.ok) {
|
||||||
console.log(data2);
|
images = images.map((image) => {
|
||||||
if (data2.type === 'success') {
|
if (image.id === image_id) {
|
||||||
images = [...images, data2];
|
image.is_primary = true;
|
||||||
|
} else {
|
||||||
|
image.is_primary = false;
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
});
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
addToast('success', $t('adventures.image_upload_success'));
|
|
||||||
} else {
|
} else {
|
||||||
|
console.error('Error in makePrimaryImage:', res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchImage() {
|
||||||
|
try {
|
||||||
|
let res = await fetch(url);
|
||||||
|
let data = await res.blob();
|
||||||
|
if (!data) {
|
||||||
|
imageError = $t('adventures.no_image_url');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let file = new File([data], 'image.jpg', { type: 'image/jpeg' });
|
||||||
|
let formData = new FormData();
|
||||||
|
formData.append('image', file);
|
||||||
|
formData.append('adventure', adventure.id);
|
||||||
|
|
||||||
|
let res2 = await fetch(`/adventures?/image`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
let data2 = await res2.json();
|
||||||
|
|
||||||
|
if (data2.type === 'success') {
|
||||||
|
console.log('Response Data:', data2);
|
||||||
|
|
||||||
|
// Deserialize the nested data
|
||||||
|
let rawData = JSON.parse(data2.data); // Parse the data field
|
||||||
|
console.log('Deserialized Data:', rawData);
|
||||||
|
|
||||||
|
// Assuming the first object in the array is the new image
|
||||||
|
let newImage = {
|
||||||
|
id: rawData[1],
|
||||||
|
image: rawData[2], // This is the URL for the image
|
||||||
|
is_primary: false
|
||||||
|
};
|
||||||
|
console.log('New Image:', newImage);
|
||||||
|
|
||||||
|
// Update images and adventure
|
||||||
|
images = [...images, newImage];
|
||||||
|
adventure.images = images;
|
||||||
|
|
||||||
|
addToast('success', $t('adventures.image_upload_success'));
|
||||||
|
url = '';
|
||||||
|
} else {
|
||||||
|
addToast('error', $t('adventures.image_upload_error'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in fetchImage:', error);
|
||||||
addToast('error', $t('adventures.image_upload_error'));
|
addToast('error', $t('adventures.image_upload_error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,7 +299,7 @@
|
||||||
if (res2.ok) {
|
if (res2.ok) {
|
||||||
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
|
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
|
||||||
console.log(newData);
|
console.log(newData);
|
||||||
let newImage = { id: newData.data.id, image: newData.data.image };
|
let newImage = { id: newData.data.id, image: newData.data.image, is_primary: false };
|
||||||
console.log(newImage);
|
console.log(newImage);
|
||||||
images = [...images, newImage];
|
images = [...images, newImage];
|
||||||
adventure.images = images;
|
adventure.images = images;
|
||||||
|
@ -337,6 +409,8 @@
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
let modal: HTMLDialogElement;
|
let modal: HTMLDialogElement;
|
||||||
|
|
||||||
|
let immichIntegration: boolean = false;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
modal.showModal();
|
modal.showModal();
|
||||||
|
@ -347,6 +421,16 @@
|
||||||
} else {
|
} else {
|
||||||
addToast('error', $t('adventures.category_fetch_error'));
|
addToast('error', $t('adventures.category_fetch_error'));
|
||||||
}
|
}
|
||||||
|
// Check for Immich Integration
|
||||||
|
let res = await fetch('/api/integrations');
|
||||||
|
if (!res.ok) {
|
||||||
|
addToast('error', $t('immich.integration_fetch_error'));
|
||||||
|
} else {
|
||||||
|
let data = await res.json();
|
||||||
|
if (data.immich) {
|
||||||
|
immichIntegration = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
@ -458,6 +542,10 @@
|
||||||
addToast('error', $t('adventures.adventure_update_error'));
|
addToast('error', $t('adventures.adventure_update_error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (adventure.is_visited && !reverseGeocodePlace?.is_visited) {
|
||||||
|
markVisited();
|
||||||
|
}
|
||||||
|
imageSearch = adventure.name;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -704,7 +792,12 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
: $t('adventures.not_visited')}
|
: $t('adventures.not_visited')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{#if !reverseGeocodePlace.is_visited}
|
{#if !reverseGeocodePlace.is_visited && !willBeMarkedVisited}
|
||||||
|
<button type="button" class="btn btn-neutral" on:click={markVisited}>
|
||||||
|
{$t('adventures.mark_visited')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if !reverseGeocodePlace.is_visited && willBeMarkedVisited}
|
||||||
<div role="alert" class="alert alert-info mt-2">
|
<div role="alert" class="alert alert-info mt-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -720,16 +813,10 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span
|
<span
|
||||||
>{$t('adventures.mark_region_as_visited', {
|
>{reverseGeocodePlace.region},
|
||||||
values: {
|
{reverseGeocodePlace.country}
|
||||||
region: reverseGeocodePlace.region,
|
{$t('adventures.will_be_marked')}</span
|
||||||
country: reverseGeocodePlace.country
|
|
||||||
}
|
|
||||||
})}</span
|
|
||||||
>
|
>
|
||||||
<button type="button" class="btn btn-neutral" on:click={markVisited}>
|
|
||||||
{$t('adventures.mark_visited')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -915,84 +1002,129 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p>{$t('adventures.upload_images_here')}</p>
|
<p class="text-lg">{$t('adventures.upload_images_here')}</p>
|
||||||
<!-- <p>{adventureToEdit.id}</p> -->
|
|
||||||
<div class="mb-2">
|
<div class="mb-4">
|
||||||
<label for="image">{$t('adventures.image')} </label><br />
|
<label for="image" class="block font-medium mb-2">
|
||||||
<div class="flex">
|
{$t('adventures.image')}
|
||||||
<form
|
</label>
|
||||||
method="POST"
|
<form
|
||||||
action="/adventures?/image"
|
method="POST"
|
||||||
use:enhance={imageSubmit}
|
action="/adventures?/image"
|
||||||
enctype="multipart/form-data"
|
use:enhance={imageSubmit}
|
||||||
>
|
enctype="multipart/form-data"
|
||||||
<input
|
class="flex flex-col items-start gap-2"
|
||||||
type="file"
|
>
|
||||||
name="image"
|
<input
|
||||||
class="file-input file-input-bordered w-full max-w-xs"
|
type="file"
|
||||||
bind:this={fileInput}
|
name="image"
|
||||||
accept="image/*"
|
class="file-input file-input-bordered w-full max-w-sm"
|
||||||
id="image"
|
bind:this={fileInput}
|
||||||
/>
|
accept="image/*"
|
||||||
<input type="hidden" name="adventure" value={adventure.id} id="adventure" />
|
id="image"
|
||||||
<button class="btn btn-neutral mt-2 mb-2" type="submit"
|
/>
|
||||||
>{$t('adventures.upload_image')}</button
|
<input type="hidden" name="adventure" value={adventure.id} id="adventure" />
|
||||||
>
|
<button class="btn btn-neutral w-full max-w-sm" type="submit">
|
||||||
</form>
|
{$t('adventures.upload_image')}
|
||||||
</div>
|
</button>
|
||||||
<div class="mt-2">
|
</form>
|
||||||
<label for="url">{$t('adventures.url')}</label><br />
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="url" class="block font-medium mb-2">
|
||||||
|
{$t('adventures.url')}
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="url"
|
id="url"
|
||||||
name="url"
|
name="url"
|
||||||
bind:value={url}
|
bind:value={url}
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered flex-1"
|
||||||
|
placeholder="Enter image URL"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-neutral mt-2" type="button" on:click={fetchImage}
|
<button class="btn btn-neutral" type="button" on:click={fetchImage}>
|
||||||
>{$t('adventures.fetch_image')}</button
|
{$t('adventures.fetch_image')}
|
||||||
>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
</div>
|
||||||
<label for="name">{$t('adventures.wikipedia')}</label><br />
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="name" class="block font-medium mb-2">
|
||||||
|
{$t('adventures.wikipedia')}
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
bind:value={imageSearch}
|
bind:value={imageSearch}
|
||||||
class="input input-bordered w-full"
|
class="input input-bordered flex-1"
|
||||||
|
placeholder="Search Wikipedia for images"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-neutral mt-2" type="button" on:click={fetchWikiImage}
|
<button class="btn btn-neutral" type="button" on:click={fetchWikiImage}>
|
||||||
>{$t('adventures.fetch_image')}</button
|
{$t('adventures.fetch_image')}
|
||||||
>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
</div>
|
||||||
{#if images.length > 0}
|
|
||||||
<h1 class="font-semibold text-xl">{$t('adventures.my_images')}</h1>
|
{#if immichIntegration}
|
||||||
{:else}
|
<ImmichSelect
|
||||||
<h1 class="font-semibold text-xl">{$t('adventures.no_images')}</h1>
|
on:fetchImage={(e) => {
|
||||||
{/if}
|
url = e.detail;
|
||||||
<div class="flex flex-wrap gap-2 mt-2">
|
fetchImage();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
{#if images.length > 0}
|
||||||
|
<h1 class="font-semibold text-xl mb-4">{$t('adventures.my_images')}</h1>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
{#each images as image}
|
{#each images as image}
|
||||||
<div class="relative h-32 w-32">
|
<div class="relative h-32 w-32">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute top-0 left-0 btn btn-error btn-sm z-10"
|
class="absolute top-1 right-1 btn btn-error btn-xs z-10"
|
||||||
on:click={() => removeImage(image.id)}
|
on:click={() => removeImage(image.id)}
|
||||||
>
|
>
|
||||||
X
|
✕
|
||||||
</button>
|
</button>
|
||||||
<img src={image.image} alt={image.id} class="w-full h-full object-cover" />
|
{#if !image.is_primary}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-1 left-1 btn btn-success btn-xs z-10"
|
||||||
|
on:click={() => makePrimaryImage(image.id)}
|
||||||
|
>
|
||||||
|
<Star class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<!-- crown icon -->
|
||||||
|
|
||||||
|
<div class="absolute top-1 left-1 bg-warning text-white rounded-full p-1 z-10">
|
||||||
|
<Crown class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<img
|
||||||
|
src={image.image}
|
||||||
|
alt={image.id}
|
||||||
|
class="w-full h-full object-cover rounded-md shadow-md"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
<div class="mt-4">
|
<h1 class="font-semibold text-xl text-gray-500">{$t('adventures.no_images')}</h1>
|
||||||
<button type="button" class="btn btn-primary" on:click={saveAndClose}
|
{/if}
|
||||||
>{$t('about.close')}</button
|
|
||||||
>
|
<div class="mt-6">
|
||||||
|
<button type="button" class="btn btn-primary w-full max-w-sm" on:click={saveAndClose}>
|
||||||
|
{$t('about.close')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if adventure.is_public && adventure.id}
|
{#if adventure.is_public && adventure.id}
|
||||||
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm">
|
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm">
|
||||||
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>
|
<p class=" font-semibold">{$t('adventures.share_adventure')}</p>
|
||||||
|
|
|
@ -9,7 +9,11 @@
|
||||||
let image_url: string | null = null;
|
let image_url: string | null = null;
|
||||||
|
|
||||||
$: adventure_images = adventures.flatMap((adventure) =>
|
$: adventure_images = adventures.flatMap((adventure) =>
|
||||||
adventure.images.map((image) => ({ image: image.image, adventure: adventure }))
|
adventure.images.map((image) => ({
|
||||||
|
image: image.image,
|
||||||
|
adventure: adventure,
|
||||||
|
is_primary: image.is_primary
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
@ -18,6 +22,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// sort so that any image in adventure_images .is_primary is first
|
||||||
|
adventure_images.sort((a, b) => {
|
||||||
|
if (a.is_primary && !b.is_primary) {
|
||||||
|
return -1;
|
||||||
|
} else if (!a.is_primary && b.is_primary) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function changeSlide(direction: string) {
|
function changeSlide(direction: string) {
|
||||||
if (direction === 'next' && currentSlide < adventure_images.length - 1) {
|
if (direction === 'next' && currentSlide < adventure_images.length - 1) {
|
||||||
currentSlide = currentSlide + 1;
|
currentSlide = currentSlide + 1;
|
||||||
|
|
175
frontend/src/lib/components/ImmichSelect.svelte
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let immichSearchValue: string = '';
|
||||||
|
let searchOrSelect: string = 'search';
|
||||||
|
let immichError: string = '';
|
||||||
|
let immichNext: string = '';
|
||||||
|
let immichPage: number = 1;
|
||||||
|
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let albums: ImmichAlbum[] = [];
|
||||||
|
let currentAlbum: string = '';
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (currentAlbum) {
|
||||||
|
immichImages = [];
|
||||||
|
fetchAlbumAssets(currentAlbum);
|
||||||
|
} else {
|
||||||
|
immichImages = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAlbumAssets(album_id: string) {
|
||||||
|
let res = await fetch(`/api/integrations/immich/albums/${album_id}`);
|
||||||
|
if (res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
immichNext = '';
|
||||||
|
immichImages = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
let res = await fetch('/api/integrations/immich/albums');
|
||||||
|
if (res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
albums = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let immichImages: any[] = [];
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import ImmichLogo from '$lib/assets/immich.svg';
|
||||||
|
import type { ImmichAlbum } from '$lib/types';
|
||||||
|
|
||||||
|
async function searchImmich() {
|
||||||
|
let res = await fetch(`/api/integrations/immich/search/?query=${immichSearchValue}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
let errorMessage = data.message;
|
||||||
|
console.log(errorMessage);
|
||||||
|
immichError = $t(data.code);
|
||||||
|
} else {
|
||||||
|
let data = await res.json();
|
||||||
|
console.log(data);
|
||||||
|
immichError = '';
|
||||||
|
if (data.results && data.results.length > 0) {
|
||||||
|
immichImages = data.results;
|
||||||
|
} else {
|
||||||
|
immichError = $t('immich.no_items_found');
|
||||||
|
}
|
||||||
|
if (data.next) {
|
||||||
|
immichNext =
|
||||||
|
'/api/integrations/immich/search?query=' +
|
||||||
|
immichSearchValue +
|
||||||
|
'&page=' +
|
||||||
|
(immichPage + 1);
|
||||||
|
} else {
|
||||||
|
immichNext = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoreImmich() {
|
||||||
|
let res = await fetch(immichNext);
|
||||||
|
if (!res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
let errorMessage = data.message;
|
||||||
|
console.log(errorMessage);
|
||||||
|
immichError = $t(data.code);
|
||||||
|
} else {
|
||||||
|
let data = await res.json();
|
||||||
|
console.log(data);
|
||||||
|
immichError = '';
|
||||||
|
if (data.results && data.results.length > 0) {
|
||||||
|
immichImages = [...immichImages, ...data.results];
|
||||||
|
} else {
|
||||||
|
immichError = $t('immich.no_items_found');
|
||||||
|
}
|
||||||
|
if (data.next) {
|
||||||
|
immichNext =
|
||||||
|
'/api/integrations/immich/search?query=' +
|
||||||
|
immichSearchValue +
|
||||||
|
'&page=' +
|
||||||
|
(immichPage + 1);
|
||||||
|
immichPage++;
|
||||||
|
} else {
|
||||||
|
immichNext = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="immich" class="block font-medium mb-2">
|
||||||
|
{$t('immich.immich')}
|
||||||
|
<img src={ImmichLogo} alt="Immich Logo" class="h-6 w-6 inline-block -mt-1" />
|
||||||
|
</label>
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="join">
|
||||||
|
<input
|
||||||
|
on:click={() => (currentAlbum = '')}
|
||||||
|
type="radio"
|
||||||
|
class="join-item btn"
|
||||||
|
bind:group={searchOrSelect}
|
||||||
|
value="search"
|
||||||
|
aria-label="Search"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="join-item btn"
|
||||||
|
bind:group={searchOrSelect}
|
||||||
|
value="select"
|
||||||
|
aria-label="Select Album"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{#if searchOrSelect === 'search'}
|
||||||
|
<form on:submit|preventDefault={searchImmich}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Type here"
|
||||||
|
bind:value={immichSearchValue}
|
||||||
|
class="input input-bordered w-full max-w-xs"
|
||||||
|
/>
|
||||||
|
<button type="submit" class="btn btn-neutral mt-2">Search</button>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<select class="select select-bordered w-full max-w-xs mt-2" bind:value={currentAlbum}>
|
||||||
|
<option value="" disabled selected>Select an Album</option>
|
||||||
|
{#each albums as album}
|
||||||
|
<option value={album.id}>{album.albumName}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-red-500">{immichError}</p>
|
||||||
|
<div class="flex flex-wrap gap-4 mr-4 mt-2">
|
||||||
|
{#each immichImages as image}
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<!-- svelte-ignore a11y-img-redundant-alt -->
|
||||||
|
<img
|
||||||
|
src={`/immich/${image.id}`}
|
||||||
|
alt="Image from Immich"
|
||||||
|
class="h-24 w-24 object-cover rounded-md"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
on:click={() => {
|
||||||
|
let currentDomain = window.location.origin;
|
||||||
|
let fullUrl = `${currentDomain}/immich/${image.id}`;
|
||||||
|
dispatch('fetchImage', fullUrl);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$t('adventures.upload_image')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if immichNext}
|
||||||
|
<button class="btn btn-neutral" on:click={loadMoreImmich}>{$t('immich.load_more')}</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -13,6 +13,18 @@
|
||||||
import { t, locale, locales } from 'svelte-i18n';
|
import { t, locale, locales } from 'svelte-i18n';
|
||||||
import { themes } from '$lib';
|
import { themes } from '$lib';
|
||||||
|
|
||||||
|
let languages: { [key: string]: string } = {
|
||||||
|
en: 'English',
|
||||||
|
de: 'Deutsch',
|
||||||
|
es: 'Español',
|
||||||
|
fr: 'Français',
|
||||||
|
it: 'Italiano',
|
||||||
|
nl: 'Nederlands',
|
||||||
|
sv: 'Svenska',
|
||||||
|
zh: '中文',
|
||||||
|
pl: 'Polski'
|
||||||
|
};
|
||||||
|
|
||||||
let query: string = '';
|
let query: string = '';
|
||||||
|
|
||||||
let isAboutModalOpen: boolean = false;
|
let isAboutModalOpen: boolean = false;
|
||||||
|
@ -236,8 +248,8 @@
|
||||||
on:change={submitLocaleChange}
|
on:change={submitLocaleChange}
|
||||||
bind:value={$locale}
|
bind:value={$locale}
|
||||||
>
|
>
|
||||||
{#each $locales as loc}
|
{#each $locales as loc (loc)}
|
||||||
<option value={loc} class="text-base-content">{$t(`languages.${loc}`)}</option>
|
<option value={loc} class="text-base-content">{languages[loc]}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<input type="hidden" name="locale" value={$locale} />
|
<input type="hidden" name="locale" value={$locale} />
|
||||||
|
|
|
@ -188,7 +188,7 @@
|
||||||
<div>
|
<div>
|
||||||
<label for="content">{$t('notes.content')}</label><br />
|
<label for="content">{$t('notes.content')}</label><br />
|
||||||
{#if !isReadOnly}
|
{#if !isReadOnly}
|
||||||
<MarkdownEditor bind:text={newNote.content} editor_height={'h-32'} />
|
<MarkdownEditor bind:text={newNote.content} editor_height={'h-72'} />
|
||||||
{:else if note}
|
{:else if note}
|
||||||
<p class="text-sm text-muted-foreground" style="white-space: pre-wrap;"></p>
|
<p class="text-sm text-muted-foreground" style="white-space: pre-wrap;"></p>
|
||||||
<article
|
<article
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export let appVersion = 'v0.7.1';
|
export let appVersion = 'v0.8.0';
|
||||||
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.7.1';
|
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.8.0';
|
||||||
export let appTitle = 'AdventureLog';
|
export let appTitle = 'AdventureLog';
|
||||||
export let copyrightYear = '2023-2025';
|
export let copyrightYear = '2023-2025';
|
||||||
|
|
|
@ -296,7 +296,7 @@ export function getRandomBackground() {
|
||||||
|
|
||||||
const newYearsStart = new Date(today.getFullYear() - 1, 11, 31);
|
const newYearsStart = new Date(today.getFullYear() - 1, 11, 31);
|
||||||
newYearsStart.setHours(0, 0, 0, 0);
|
newYearsStart.setHours(0, 0, 0, 0);
|
||||||
const newYearsEnd = new Date(today.getFullYear(), 0, 7);
|
const newYearsEnd = new Date(today.getFullYear(), 0, 2);
|
||||||
newYearsEnd.setHours(23, 59, 59, 999);
|
newYearsEnd.setHours(23, 59, 59, 999);
|
||||||
if (today >= newYearsStart && today <= newYearsEnd) {
|
if (today >= newYearsStart && today <= newYearsEnd) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -24,6 +24,7 @@ export type Adventure = {
|
||||||
images: {
|
images: {
|
||||||
id: string;
|
id: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
is_primary: boolean;
|
||||||
}[];
|
}[];
|
||||||
visits: {
|
visits: {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -50,6 +51,8 @@ export type Country = {
|
||||||
capital: string;
|
capital: string;
|
||||||
num_regions: number;
|
num_regions: number;
|
||||||
num_visits: number;
|
num_visits: number;
|
||||||
|
longitude: number | null;
|
||||||
|
latitude: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Region = {
|
export type Region = {
|
||||||
|
@ -194,3 +197,37 @@ export type Category = {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
num_adventures?: number | null;
|
num_adventures?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ImmichIntegration = {
|
||||||
|
id: string;
|
||||||
|
server_url: string;
|
||||||
|
api_key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
|
@ -215,7 +215,9 @@
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting_airport": "Startflughafen",
|
"starting_airport": "Startflughafen",
|
||||||
"to": "Zu",
|
"to": "Zu",
|
||||||
"transportation_delete_confirm": "Sind Sie sicher, dass Sie diesen Transport löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden."
|
"transportation_delete_confirm": "Sind Sie sicher, dass Sie diesen Transport löschen möchten? \nDiese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"show_map": "Karte anzeigen",
|
||||||
|
"will_be_marked": "wird als besucht markiert, sobald das Abenteuer gespeichert ist."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
|
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
|
||||||
|
@ -454,17 +456,7 @@
|
||||||
"show_visited_regions": "Besuchte Regionen anzeigen",
|
"show_visited_regions": "Besuchte Regionen anzeigen",
|
||||||
"view_details": "Details anzeigen"
|
"view_details": "Details anzeigen"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {},
|
||||||
"de": "Deutsch",
|
|
||||||
"en": "Englisch",
|
|
||||||
"es": "Spanisch",
|
|
||||||
"fr": "Französisch",
|
|
||||||
"it": "Italienisch",
|
|
||||||
"nl": "Niederländisch",
|
|
||||||
"sv": "Schwedisch",
|
|
||||||
"zh": "chinesisch",
|
|
||||||
"pl": "Polnisch"
|
|
||||||
},
|
|
||||||
"share": {
|
"share": {
|
||||||
"no_users_shared": "Keine Benutzer geteilt mit",
|
"no_users_shared": "Keine Benutzer geteilt mit",
|
||||||
"not_shared_with": "Nicht geteilt mit",
|
"not_shared_with": "Nicht geteilt mit",
|
||||||
|
@ -500,5 +492,30 @@
|
||||||
"total_adventures": "Totale Abenteuer",
|
"total_adventures": "Totale Abenteuer",
|
||||||
"total_visited_regions": "Insgesamt besuchte Regionen",
|
"total_visited_regions": "Insgesamt besuchte Regionen",
|
||||||
"welcome_back": "Willkommen zurück"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,8 +229,10 @@
|
||||||
"no_location_found": "No location found",
|
"no_location_found": "No location found",
|
||||||
"from": "From",
|
"from": "From",
|
||||||
"to": "To",
|
"to": "To",
|
||||||
|
"will_be_marked": "will be marked as visited once the adventure is saved.",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"end": "End",
|
"end": "End",
|
||||||
|
"show_map": "Show Map",
|
||||||
"emoji_picker": "Emoji Picker",
|
"emoji_picker": "Emoji Picker",
|
||||||
"download_calendar": "Download Calendar",
|
"download_calendar": "Download Calendar",
|
||||||
"date_information": "Date Information",
|
"date_information": "Date Information",
|
||||||
|
@ -466,17 +468,7 @@
|
||||||
"set_public": "In order to allow users to share with you, you need your profile set to public.",
|
"set_public": "In order to allow users to share with you, you need your profile set to public.",
|
||||||
"go_to_settings": "Go to settings"
|
"go_to_settings": "Go to settings"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {},
|
||||||
"en": "English",
|
|
||||||
"de": "German",
|
|
||||||
"es": "Spanish",
|
|
||||||
"fr": "French",
|
|
||||||
"it": "Italian",
|
|
||||||
"nl": "Dutch",
|
|
||||||
"sv": "Swedish",
|
|
||||||
"zh": "Chinese",
|
|
||||||
"pl": "Polish"
|
|
||||||
},
|
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Member since",
|
"member_since": "Member since",
|
||||||
"user_stats": "User Stats",
|
"user_stats": "User Stats",
|
||||||
|
@ -500,5 +492,30 @@
|
||||||
"recent_adventures": "Recent Adventures",
|
"recent_adventures": "Recent Adventures",
|
||||||
"no_recent_adventures": "No 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."
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,7 +262,9 @@
|
||||||
"start": "Comenzar",
|
"start": "Comenzar",
|
||||||
"starting_airport": "Aeropuerto de inicio",
|
"starting_airport": "Aeropuerto de inicio",
|
||||||
"to": "A",
|
"to": "A",
|
||||||
"transportation_delete_confirm": "¿Está seguro de que desea eliminar este transporte? \nEsta acción no se puede deshacer."
|
"transportation_delete_confirm": "¿Está seguro de que desea eliminar este transporte? \nEsta acción no se puede deshacer.",
|
||||||
|
"show_map": "Mostrar mapa",
|
||||||
|
"will_be_marked": "se marcará como visitado una vez guardada la aventura."
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"all": "Todo",
|
"all": "Todo",
|
||||||
|
@ -466,17 +468,7 @@
|
||||||
"no_shared_found": "No se encontraron colecciones que se compartan contigo.",
|
"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."
|
"set_public": "Para permitir que los usuarios compartan contenido con usted, necesita que su perfil esté configurado como público."
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {},
|
||||||
"de": "Alemán",
|
|
||||||
"en": "Inglés",
|
|
||||||
"es": "Español",
|
|
||||||
"fr": "Francés",
|
|
||||||
"it": "italiano",
|
|
||||||
"nl": "Holandés",
|
|
||||||
"sv": "sueco",
|
|
||||||
"zh": "Chino",
|
|
||||||
"pl": "Polaco"
|
|
||||||
},
|
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Miembro desde",
|
"member_since": "Miembro desde",
|
||||||
"user_stats": "Estadísticas de usuario",
|
"user_stats": "Estadísticas de usuario",
|
||||||
|
@ -500,5 +492,30 @@
|
||||||
"total_adventures": "Aventuras totales",
|
"total_adventures": "Aventuras totales",
|
||||||
"total_visited_regions": "Total de regiones visitadas",
|
"total_visited_regions": "Total de regiones visitadas",
|
||||||
"welcome_back": "Bienvenido de nuevo"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,9 @@
|
||||||
"start": "Commencer",
|
"start": "Commencer",
|
||||||
"starting_airport": "Aéroport de départ",
|
"starting_airport": "Aéroport de départ",
|
||||||
"to": "À",
|
"to": "À",
|
||||||
"transportation_delete_confirm": "Etes-vous sûr de vouloir supprimer ce transport ? \nCette action ne peut pas être annulée."
|
"transportation_delete_confirm": "Etes-vous sûr de vouloir supprimer ce transport ? \nCette action ne peut pas être annulée.",
|
||||||
|
"show_map": "Afficher la carte",
|
||||||
|
"will_be_marked": "sera marqué comme visité une fois l’aventure sauvegardée."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
||||||
|
@ -454,17 +456,7 @@
|
||||||
"show_visited_regions": "Afficher les régions visitées",
|
"show_visited_regions": "Afficher les régions visitées",
|
||||||
"view_details": "Afficher les détails"
|
"view_details": "Afficher les détails"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {},
|
||||||
"de": "Allemand",
|
|
||||||
"en": "Anglais",
|
|
||||||
"es": "Espagnol",
|
|
||||||
"fr": "Français",
|
|
||||||
"it": "italien",
|
|
||||||
"nl": "Néerlandais",
|
|
||||||
"sv": "suédois",
|
|
||||||
"zh": "Chinois",
|
|
||||||
"pl": "Polonais"
|
|
||||||
},
|
|
||||||
"share": {
|
"share": {
|
||||||
"no_users_shared": "Aucun utilisateur partagé avec",
|
"no_users_shared": "Aucun utilisateur partagé avec",
|
||||||
"not_shared_with": "Non partagé avec",
|
"not_shared_with": "Non partagé avec",
|
||||||
|
@ -500,5 +492,30 @@
|
||||||
"total_adventures": "Aventures totales",
|
"total_adventures": "Aventures totales",
|
||||||
"total_visited_regions": "Total des régions visitées",
|
"total_visited_regions": "Total des régions visitées",
|
||||||
"welcome_back": "Content de te revoir"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,9 @@
|
||||||
"start": "Inizio",
|
"start": "Inizio",
|
||||||
"starting_airport": "Inizio aeroporto",
|
"starting_airport": "Inizio aeroporto",
|
||||||
"to": "A",
|
"to": "A",
|
||||||
"transportation_delete_confirm": "Sei sicuro di voler eliminare questo trasporto? \nQuesta azione non può essere annullata."
|
"transportation_delete_confirm": "Sei sicuro di voler eliminare questo trasporto? \nQuesta azione non può essere annullata.",
|
||||||
|
"show_map": "Mostra mappa",
|
||||||
|
"will_be_marked": "verrà contrassegnato come visitato una volta salvata l'avventura."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
||||||
|
@ -454,17 +456,7 @@
|
||||||
"show_visited_regions": "Mostra regioni visitate",
|
"show_visited_regions": "Mostra regioni visitate",
|
||||||
"view_details": "Visualizza dettagli"
|
"view_details": "Visualizza dettagli"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {},
|
||||||
"de": "tedesco",
|
|
||||||
"en": "Inglese",
|
|
||||||
"es": "spagnolo",
|
|
||||||
"fr": "francese",
|
|
||||||
"it": "Italiano",
|
|
||||||
"nl": "Olandese",
|
|
||||||
"sv": "svedese",
|
|
||||||
"zh": "cinese",
|
|
||||||
"pl": "Polacco"
|
|
||||||
},
|
|
||||||
"share": {
|
"share": {
|
||||||
"no_users_shared": "Nessun utente condiviso con",
|
"no_users_shared": "Nessun utente condiviso con",
|
||||||
"not_shared_with": "Non condiviso con",
|
"not_shared_with": "Non condiviso con",
|
||||||
|
@ -500,5 +492,30 @@
|
||||||
"total_adventures": "Avventure totali",
|
"total_adventures": "Avventure totali",
|
||||||
"total_visited_regions": "Totale regioni visitate",
|
"total_visited_regions": "Totale regioni visitate",
|
||||||
"welcome_back": "Bentornato"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,9 @@
|
||||||
"starting_airport": "Startende luchthaven",
|
"starting_airport": "Startende luchthaven",
|
||||||
"to": "Naar",
|
"to": "Naar",
|
||||||
"transportation_delete_confirm": "Weet u zeker dat u dit transport wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
"transportation_delete_confirm": "Weet u zeker dat u dit transport wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||||
"ending_airport": "Einde luchthaven"
|
"ending_airport": "Einde luchthaven",
|
||||||
|
"show_map": "Toon kaart",
|
||||||
|
"will_be_marked": "wordt gemarkeerd als bezocht zodra het avontuur is opgeslagen."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Ontdek, plan en verken met gemak",
|
"desc_1": "Ontdek, plan en verken met gemak",
|
||||||
|
@ -454,17 +456,7 @@
|
||||||
"show_visited_regions": "Toon bezochte regio's",
|
"show_visited_regions": "Toon bezochte regio's",
|
||||||
"view_details": "Details bekijken"
|
"view_details": "Details bekijken"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {},
|
||||||
"de": "Duits",
|
|
||||||
"en": "Engels",
|
|
||||||
"es": "Spaans",
|
|
||||||
"fr": "Frans",
|
|
||||||
"it": "Italiaans",
|
|
||||||
"nl": "Nederlands",
|
|
||||||
"sv": "Zweeds",
|
|
||||||
"zh": "Chinese",
|
|
||||||
"pl": "Pools"
|
|
||||||
},
|
|
||||||
"share": {
|
"share": {
|
||||||
"no_users_shared": "Er zijn geen gebruikers gedeeld",
|
"no_users_shared": "Er zijn geen gebruikers gedeeld",
|
||||||
"not_shared_with": "Niet gedeeld met",
|
"not_shared_with": "Niet gedeeld met",
|
||||||
|
@ -500,5 +492,30 @@
|
||||||
"total_adventures": "Totale avonturen",
|
"total_adventures": "Totale avonturen",
|
||||||
"total_visited_regions": "Totaal bezochte regio's",
|
"total_visited_regions": "Totaal bezochte regio's",
|
||||||
"welcome_back": "Welkom terug"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,7 +262,9 @@
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting_airport": "Początkowe lotnisko",
|
"starting_airport": "Początkowe lotnisko",
|
||||||
"to": "Do",
|
"to": "Do",
|
||||||
"transportation_delete_confirm": "Czy na pewno chcesz usunąć ten transport? \nTej akcji nie można cofnąć."
|
"transportation_delete_confirm": "Czy na pewno chcesz usunąć ten transport? \nTej akcji nie można cofnąć.",
|
||||||
|
"show_map": "Pokaż mapę",
|
||||||
|
"will_be_marked": "zostanie oznaczona jako odwiedzona po zapisaniu przygody."
|
||||||
},
|
},
|
||||||
"worldtravel": {
|
"worldtravel": {
|
||||||
"country_list": "Lista krajów",
|
"country_list": "Lista krajów",
|
||||||
|
@ -466,17 +468,7 @@
|
||||||
"set_public": "Aby umożliwić użytkownikom udostępnianie Tobie, musisz ustawić swój profil jako publiczny.",
|
"set_public": "Aby umożliwić użytkownikom udostępnianie Tobie, musisz ustawić swój profil jako publiczny.",
|
||||||
"go_to_settings": "Przejdź do ustawień"
|
"go_to_settings": "Przejdź do ustawień"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {},
|
||||||
"en": "Angielski",
|
|
||||||
"de": "Niemiecki",
|
|
||||||
"es": "Hiszpański",
|
|
||||||
"fr": "Francuski",
|
|
||||||
"it": "Włoski",
|
|
||||||
"nl": "Holenderski",
|
|
||||||
"sv": "Szwedzki",
|
|
||||||
"zh": "Chiński",
|
|
||||||
"pl": "Polski"
|
|
||||||
},
|
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Użytkownik od",
|
"member_since": "Użytkownik od",
|
||||||
"user_stats": "Statystyki użytkownika",
|
"user_stats": "Statystyki użytkownika",
|
||||||
|
@ -500,5 +492,30 @@
|
||||||
"total_adventures": "Totalne przygody",
|
"total_adventures": "Totalne przygody",
|
||||||
"total_visited_regions": "Łączna liczba odwiedzonych regionów",
|
"total_visited_regions": "Łączna liczba odwiedzonych regionów",
|
||||||
"welcome_back": "Witamy z powrotem"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
|
|
||||||
"about": {
|
"about": {
|
||||||
"about": "Om",
|
"about": "Om",
|
||||||
"close": "Stäng",
|
"close": "Stäng",
|
||||||
|
@ -216,7 +215,9 @@
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting_airport": "Startar flygplats",
|
"starting_airport": "Startar flygplats",
|
||||||
"to": "Till",
|
"to": "Till",
|
||||||
"transportation_delete_confirm": "Är du säker på att du vill ta bort denna transport? \nDenna åtgärd kan inte ångras."
|
"transportation_delete_confirm": "Är du säker på att du vill ta bort denna transport? \nDenna åtgärd kan inte ångras.",
|
||||||
|
"show_map": "Visa karta",
|
||||||
|
"will_be_marked": "kommer att markeras som besökt när äventyret har sparats."
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "Upptäck, planera och utforska med lätthet",
|
"desc_1": "Upptäck, planera och utforska med lätthet",
|
||||||
|
@ -455,17 +456,7 @@
|
||||||
"show_visited_regions": "Visa besökta regioner",
|
"show_visited_regions": "Visa besökta regioner",
|
||||||
"view_details": "Visa detaljer"
|
"view_details": "Visa detaljer"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {},
|
||||||
"de": "tyska",
|
|
||||||
"en": "engelska",
|
|
||||||
"es": "spanska",
|
|
||||||
"fr": "franska",
|
|
||||||
"it": "italienska",
|
|
||||||
"nl": "holländska",
|
|
||||||
"sv": "svenska",
|
|
||||||
"zh": "kinesiska",
|
|
||||||
"pl": "polska"
|
|
||||||
},
|
|
||||||
"share": {
|
"share": {
|
||||||
"no_users_shared": "Inga användare delas med",
|
"no_users_shared": "Inga användare delas med",
|
||||||
"not_shared_with": "Inte delad med",
|
"not_shared_with": "Inte delad med",
|
||||||
|
@ -501,5 +492,30 @@
|
||||||
"total_adventures": "Totala äventyr",
|
"total_adventures": "Totala äventyr",
|
||||||
"total_visited_regions": "Totalt antal besökta regioner",
|
"total_visited_regions": "Totalt antal besökta regioner",
|
||||||
"welcome_back": "Välkommen tillbaka"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,7 +215,9 @@
|
||||||
"start": "开始",
|
"start": "开始",
|
||||||
"starting_airport": "出发机场",
|
"starting_airport": "出发机场",
|
||||||
"to": "到",
|
"to": "到",
|
||||||
"transportation_delete_confirm": "您确定要删除此交通工具吗?\n此操作无法撤消。"
|
"transportation_delete_confirm": "您确定要删除此交通工具吗?\n此操作无法撤消。",
|
||||||
|
"show_map": "显示地图",
|
||||||
|
"will_be_marked": "保存冒险后将被标记为已访问。"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"desc_1": "轻松发现、规划和探索",
|
"desc_1": "轻松发现、规划和探索",
|
||||||
|
@ -454,17 +456,7 @@
|
||||||
"show_visited_regions": "显示访问过的地区",
|
"show_visited_regions": "显示访问过的地区",
|
||||||
"view_details": "查看详情"
|
"view_details": "查看详情"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {},
|
||||||
"de": "德语",
|
|
||||||
"en": "英语",
|
|
||||||
"es": "西班牙语",
|
|
||||||
"fr": "法语",
|
|
||||||
"it": "意大利语",
|
|
||||||
"nl": "荷兰语",
|
|
||||||
"sv": "瑞典",
|
|
||||||
"zh": "中国人",
|
|
||||||
"pl": "波兰语"
|
|
||||||
},
|
|
||||||
"share": {
|
"share": {
|
||||||
"no_users_shared": "没有与之共享的用户",
|
"no_users_shared": "没有与之共享的用户",
|
||||||
"not_shared_with": "不与共享",
|
"not_shared_with": "不与共享",
|
||||||
|
@ -500,5 +492,30 @@
|
||||||
"total_adventures": "全面冒险",
|
"total_adventures": "全面冒险",
|
||||||
"total_visited_regions": "总访问地区",
|
"total_visited_regions": "总访问地区",
|
||||||
"welcome_back": "欢迎回来"
|
"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地址或域名。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,16 @@
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (data.props.adventure) {
|
if (data.props.adventure) {
|
||||||
adventure = 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 {
|
} else {
|
||||||
notFound = true;
|
notFound = true;
|
||||||
}
|
}
|
||||||
|
|
54
frontend/src/routes/immich/[key]/+server.ts
Normal file
|
@ -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' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from '../$types';
|
import type { PageServerLoad } from '../$types';
|
||||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
import type { User } from '$lib/types';
|
import type { ImmichIntegration, User } from '$lib/types';
|
||||||
import { fetchCSRFToken } from '$lib/index.server';
|
import { fetchCSRFToken } from '$lib/index.server';
|
||||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
|
@ -56,11 +56,22 @@ export const load: PageServerLoad = async (event) => {
|
||||||
let mfaAuthenticatorResponse = (await mfaAuthenticatorFetch.json()) as MFAAuthenticatorResponse;
|
let mfaAuthenticatorResponse = (await mfaAuthenticatorFetch.json()) as MFAAuthenticatorResponse;
|
||||||
let authenticators = (mfaAuthenticatorResponse.data.length > 0) as boolean;
|
let authenticators = (mfaAuthenticatorResponse.data.length > 0) as boolean;
|
||||||
|
|
||||||
|
let immichIntegration: ImmichIntegration | null = null;
|
||||||
|
let immichIntegrationsFetch = await fetch(`${endpoint}/api/integrations/immich/`, {
|
||||||
|
headers: {
|
||||||
|
Cookie: `sessionid=${sessionId}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (immichIntegrationsFetch.ok) {
|
||||||
|
immichIntegration = await immichIntegrationsFetch.json();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
user,
|
user,
|
||||||
emails,
|
emails,
|
||||||
authenticators
|
authenticators,
|
||||||
|
immichIntegration
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,14 +2,16 @@
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
import type { User } from '$lib/types.js';
|
import type { ImmichIntegration, User } from '$lib/types.js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import TotpModal from '$lib/components/TOTPModal.svelte';
|
import TotpModal from '$lib/components/TOTPModal.svelte';
|
||||||
import { appTitle, appVersion } from '$lib/config.js';
|
import { appTitle, appVersion } from '$lib/config.js';
|
||||||
|
import ImmichLogo from '$lib/assets/immich.svg';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
console.log(data);
|
||||||
let user: User;
|
let user: User;
|
||||||
let emails: typeof data.props.emails;
|
let emails: typeof data.props.emails;
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
|
@ -19,6 +21,14 @@
|
||||||
|
|
||||||
let new_email: string = '';
|
let new_email: string = '';
|
||||||
|
|
||||||
|
let immichIntegration = data.props.immichIntegration;
|
||||||
|
|
||||||
|
let newImmichIntegration: ImmichIntegration = {
|
||||||
|
server_url: '',
|
||||||
|
api_key: '',
|
||||||
|
id: ''
|
||||||
|
};
|
||||||
|
|
||||||
let isMFAModalOpen: boolean = false;
|
let isMFAModalOpen: boolean = false;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
@ -131,6 +141,54 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function enableImmichIntegration() {
|
||||||
|
if (!immichIntegration?.id) {
|
||||||
|
let res = await fetch('/api/integrations/immich/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newImmichIntegration)
|
||||||
|
});
|
||||||
|
let data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
addToast('success', $t('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() {
|
async function disableMfa() {
|
||||||
const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', {
|
const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
|
@ -174,7 +232,9 @@
|
||||||
class="space-y-6"
|
class="space-y-6"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label for="username" class="text-sm font-medium">{$t('auth.username')}</label>
|
<label for="username" class="text-sm font-medium text-neutral-content"
|
||||||
|
>{$t('auth.username')}</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
|
@ -185,7 +245,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="first_name" class="text-sm font-medium">{$t('auth.first_name')}</label>
|
<label for="first_name" class="text-sm font-medium text-neutral-content"
|
||||||
|
>{$t('auth.first_name')}</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="first_name"
|
id="first_name"
|
||||||
|
@ -196,7 +258,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="last_name" class="text-sm font-medium">{$t('auth.last_name')}</label>
|
<label for="last_name" class="text-sm font-medium text-neutral-content"
|
||||||
|
>{$t('auth.last_name')}</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="last_name"
|
id="last_name"
|
||||||
|
@ -207,7 +271,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="profile_pic" class="text-sm font-medium">{$t('auth.profile_picture')}</label>
|
<label for="profile_pic" class="text-sm font-medium text-neutral-content"
|
||||||
|
>{$t('auth.profile_picture')}</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="profile_pic"
|
id="profile_pic"
|
||||||
|
@ -224,7 +290,9 @@
|
||||||
bind:checked={user.public_profile}
|
bind:checked={user.public_profile}
|
||||||
class="toggle toggle-primary"
|
class="toggle toggle-primary"
|
||||||
/>
|
/>
|
||||||
<label for="public_profile" class="ml-2 text-sm">{$t('auth.public_profile')}</label>
|
<label for="public_profile" class="ml-2 text-sm text-neutral-content"
|
||||||
|
>{$t('auth.public_profile')}</label
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="w-full mt-4 btn btn-primary py-2">{$t('settings.update')}</button>
|
<button class="w-full mt-4 btn btn-primary py-2">{$t('settings.update')}</button>
|
||||||
|
@ -240,7 +308,7 @@
|
||||||
<div class="bg-neutral p-6 rounded-lg shadow-md">
|
<div class="bg-neutral p-6 rounded-lg shadow-md">
|
||||||
<form method="post" action="?/changePassword" use:enhance class="space-y-6">
|
<form method="post" action="?/changePassword" use:enhance class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label for="current_password" class="text-sm font-medium"
|
<label for="current_password" class="text-sm font-medium text-neutral-content"
|
||||||
>{$t('settings.current_password')}</label
|
>{$t('settings.current_password')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
@ -252,7 +320,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="password1" class="text-sm font-medium">{$t('settings.new_password')}</label>
|
<label for="password1" class="text-sm font-medium text-neutral-content"
|
||||||
|
>{$t('settings.new_password')}</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="password1"
|
id="password1"
|
||||||
|
@ -262,7 +332,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="password2" class="text-sm font-medium"
|
<label for="password2" class="text-sm font-medium text-neutral-content"
|
||||||
>{$t('settings.confirm_new_password')}</label
|
>{$t('settings.confirm_new_password')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
@ -317,7 +387,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if emails.length === 0}
|
{#if emails.length === 0}
|
||||||
<p class="text-center">{$t('settings.no_email_set')}</p>
|
<p class="text-center text-neutral-content">{$t('settings.no_email_set')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -342,7 +412,7 @@
|
||||||
</h2>
|
</h2>
|
||||||
<div class="bg-neutral p-6 rounded-lg shadow-md text-center">
|
<div class="bg-neutral p-6 rounded-lg shadow-md text-center">
|
||||||
{#if !data.props.authenticators}
|
{#if !data.props.authenticators}
|
||||||
<p>{$t('settings.mfa_not_enabled')}</p>
|
<p class="text-neutral-content">{$t('settings.mfa_not_enabled')}</p>
|
||||||
<button class="btn btn-primary mt-4" on:click={() => (isMFAModalOpen = true)}
|
<button class="btn btn-primary mt-4" on:click={() => (isMFAModalOpen = true)}
|
||||||
>{$t('settings.enable_mfa')}</button
|
>{$t('settings.enable_mfa')}</button
|
||||||
>
|
>
|
||||||
|
@ -354,6 +424,85 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Immich Integration Section -->
|
||||||
|
<section class="space-y-8">
|
||||||
|
<h2 class="text-2xl font-semibold text-center mt-8">
|
||||||
|
{$t('immich.immich_integration')}
|
||||||
|
<img src={ImmichLogo} alt="Immich" class="inline-block w-8 h-8 -mt-1" />
|
||||||
|
</h2>
|
||||||
|
<div class="bg-neutral p-6 rounded-lg shadow-md">
|
||||||
|
<p class="text-center text-neutral-content">
|
||||||
|
{$t('immich.immich_desc')}
|
||||||
|
<a
|
||||||
|
class="link link-primary"
|
||||||
|
href="https://adventurelog.app/docs/configuration/immich_integration.html"
|
||||||
|
target="_blank">{$t('immich.documentation')}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
{#if immichIntegration}
|
||||||
|
<div class="flex flex-col items-center justify-center mt-1 space-y-2">
|
||||||
|
<div class="badge badge-success">{$t('immich.integration_enabled')}</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-warning"
|
||||||
|
on:click={() => {
|
||||||
|
if (immichIntegration) newImmichIntegration = immichIntegration;
|
||||||
|
}}>Edit</button
|
||||||
|
>
|
||||||
|
<button class="btn btn-error" on:click={disableImmichIntegration}
|
||||||
|
>{$t('immich.disable')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !immichIntegration || newImmichIntegration.id}
|
||||||
|
<div class="mt-4">
|
||||||
|
<div>
|
||||||
|
<label for="immich_url" class="text-sm font-medium text-neutral-content"
|
||||||
|
>{$t('immich.server_url')}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="immich_url"
|
||||||
|
name="immich_url"
|
||||||
|
bind:value={newImmichIntegration.server_url}
|
||||||
|
placeholder="{$t('immich.server_url')} (e.g. https://immich.example.com/api)"
|
||||||
|
class="block w-full mt-1 input input-bordered input-primary"
|
||||||
|
/>
|
||||||
|
{#if newImmichIntegration.server_url && !newImmichIntegration.server_url.endsWith('api')}
|
||||||
|
<p class="text-s text-warning mt-2">
|
||||||
|
{$t('immich.api_note')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{#if newImmichIntegration.server_url && (newImmichIntegration.server_url.indexOf('localhost') !== -1 || newImmichIntegration.server_url.indexOf('127.0.0.1') !== -1)}
|
||||||
|
<p class="text-s text-warning mt-2">
|
||||||
|
{$t('immich.localhost_note')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="immich_api_key" class="text-sm font-medium text-neutral-content"
|
||||||
|
>{$t('immich.api_key')}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="immich_api_key"
|
||||||
|
name="immich_api_key"
|
||||||
|
bind:value={newImmichIntegration.api_key}
|
||||||
|
placeholder={$t('immich.api_key')}
|
||||||
|
class="block w-full mt-1 input input-bordered input-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button on:click={enableImmichIntegration} class="w-full mt-4 btn btn-primary py-2"
|
||||||
|
>{!immichIntegration?.id
|
||||||
|
? $t('immich.enable_immich')
|
||||||
|
: $t('immich.update_integration')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Visited Region Check Section -->
|
<!-- Visited Region Check Section -->
|
||||||
<section class="text-center mt-8">
|
<section class="text-center mt-8">
|
||||||
<h2 class="text-2xl font-semibold">{$t('adventures.visited_region_check')}</h2>
|
<h2 class="text-2xl font-semibold">{$t('adventures.visited_region_check')}</h2>
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import CountryCard from '$lib/components/CountryCard.svelte';
|
import CountryCard from '$lib/components/CountryCard.svelte';
|
||||||
import type { Country } from '$lib/types';
|
import type { Country } from '$lib/types';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import { MapLibre, Marker } from 'svelte-maplibre';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
@ -12,6 +14,7 @@
|
||||||
let filteredCountries: Country[] = [];
|
let filteredCountries: Country[] = [];
|
||||||
const allCountries: Country[] = data.props?.countries || [];
|
const allCountries: Country[] = data.props?.countries || [];
|
||||||
let worldSubregions: string[] = [];
|
let worldSubregions: string[] = [];
|
||||||
|
let showMap: boolean = false;
|
||||||
|
|
||||||
worldSubregions = [...new Set(allCountries.map((country) => country.subregion))];
|
worldSubregions = [...new Set(allCountries.map((country) => country.subregion))];
|
||||||
// remove blank subregions
|
// remove blank subregions
|
||||||
|
@ -96,6 +99,16 @@
|
||||||
<option value={subregion}>{subregion}</option>
|
<option value={subregion}>{subregion}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
<!-- borderd checkbox -->
|
||||||
|
<div class="flex items-center justify-center ml-4">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-bordered"
|
||||||
|
bind:checked={showMap}
|
||||||
|
aria-label={$t('adventures.show_map')}
|
||||||
|
/>
|
||||||
|
<span class="ml-2">{$t('adventures.show_map')}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center mb-4">
|
<div class="flex items-center justify-center mb-4">
|
||||||
|
@ -115,6 +128,33 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if showMap}
|
||||||
|
<div class="mt-4 mb-4 flex justify-center">
|
||||||
|
<!-- checkbox to toggle marker -->
|
||||||
|
|
||||||
|
<MapLibre
|
||||||
|
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||||
|
class="aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
|
||||||
|
standardControls
|
||||||
|
zoom={2}
|
||||||
|
>
|
||||||
|
{#each filteredCountries as country}
|
||||||
|
{#if country.latitude && country.longitude}
|
||||||
|
<Marker
|
||||||
|
lngLat={[country.longitude, country.latitude]}
|
||||||
|
class="grid px-2 py-1 place-items-center rounded-full border border-gray-200 bg-green-200 text-black focus:outline-6 focus:outline-black"
|
||||||
|
on:click={() => goto(`/worldtravel/${country.country_code}`)}
|
||||||
|
>
|
||||||
|
<span class="text-xs">
|
||||||
|
{country.name}
|
||||||
|
</span>
|
||||||
|
</Marker>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</MapLibre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
||||||
{#each filteredCountries as country}
|
{#each filteredCountries as country}
|
||||||
<CountryCard {country} />
|
<CountryCard {country} />
|
||||||
|
|
|
@ -161,7 +161,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</MapLibre>
|
</MapLibre>
|
||||||
<!-- button to clear to and from location -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 266 KiB |
Before Width: | Height: | Size: 534 KiB |
Before Width: | Height: | Size: 493 KiB |
Before Width: | Height: | Size: 410 KiB |
Before Width: | Height: | Size: 305 KiB |
Before Width: | Height: | Size: 62 KiB |