mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-24 07:19:36 +02:00
Import and Export Functionality (#698)
* feat(backup): add BackupViewSet for data export and import functionality * Fixed frontend returning corrupt binary data * feat(import): enhance import functionality with confirmation check and improved city/region/country handling * Potential fix for code scanning alert no. 29: Information exposure through an exception Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Refactor response handling to use arrayBuffer instead of bytes * Refactor image cleanup command to use LocationImage model and update import/export view to include backup and restore functionality * Update backup export versioning and improve data restore warning message * Enhance image navigation and localization support in modal components * Refactor location handling in Immich integration components for consistency * Enhance backup and restore functionality with improved localization and error handling * Improve accessibility by adding 'for' attribute to backup file input label --------- Co-authored-by: Christian Zäske <blitzdose@gmail.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
parent
493a13995c
commit
c461f7b105
31 changed files with 2843 additions and 1114 deletions
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from adventures.models import AdventureImage, Attachment
|
||||
from adventures.models import LocationImage, Attachment
|
||||
from users.models import CustomUser
|
||||
|
||||
|
||||
|
@ -21,8 +21,8 @@ class Command(BaseCommand):
|
|||
# Get all image and attachment file paths from database
|
||||
used_files = set()
|
||||
|
||||
# Get AdventureImage file paths
|
||||
for img in AdventureImage.objects.all():
|
||||
# Get LocationImage file paths
|
||||
for img in LocationImage.objects.all():
|
||||
if img.image and img.image.name:
|
||||
used_files.add(os.path.join(settings.MEDIA_ROOT, img.image.name))
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@ router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-cale
|
|||
router.register(r'search', GlobalSearchView, basename='search')
|
||||
router.register(r'attachments', AttachmentViewSet, basename='attachments')
|
||||
router.register(r'lodging', LodgingViewSet, basename='lodging')
|
||||
router.register(r'recommendations', RecommendationsViewSet, basename='recommendations')
|
||||
router.register(r'recommendations', RecommendationsViewSet, basename='recommendations'),
|
||||
router.register(r'backup', BackupViewSet, basename='backup')
|
||||
|
||||
urlpatterns = [
|
||||
# Include the router under the 'api/' prefix
|
||||
|
|
|
@ -13,4 +13,5 @@ from .transportation_view import *
|
|||
from .global_search_view import *
|
||||
from .attachment_view import *
|
||||
from .lodging_view import *
|
||||
from .recommendations_view import *
|
||||
from .recommendations_view import *
|
||||
from .import_export_view import *
|
630
backend/server/adventures/views/import_export_view.py
Normal file
630
backend/server/adventures/views/import_export_view.py
Normal file
|
@ -0,0 +1,630 @@
|
|||
# views.py
|
||||
import json
|
||||
import zipfile
|
||||
import tempfile
|
||||
import os
|
||||
from datetime import datetime
|
||||
from django.http import HttpResponse
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.conf import settings
|
||||
|
||||
from adventures.models import (
|
||||
Location, Collection, Transportation, Note, Checklist, ChecklistItem,
|
||||
LocationImage, Attachment, Category, Lodging, Visit
|
||||
)
|
||||
from worldtravel.models import VisitedCity, VisitedRegion, City, Region, Country
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class BackupViewSet(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
"""
|
||||
Simple ViewSet for handling backup and import operations
|
||||
"""
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def export(self, request):
|
||||
"""
|
||||
Export all user data as a ZIP file containing JSON data and files
|
||||
"""
|
||||
user = request.user
|
||||
|
||||
# Build export data structure
|
||||
export_data = {
|
||||
'version': settings.ADVENTURELOG_RELEASE_VERSION,
|
||||
'export_date': datetime.now().isoformat(),
|
||||
'user_email': user.email,
|
||||
'user_username': user.username,
|
||||
'categories': [],
|
||||
'collections': [],
|
||||
'locations': [],
|
||||
'transportation': [],
|
||||
'notes': [],
|
||||
'checklists': [],
|
||||
'lodging': [],
|
||||
'visited_cities': [],
|
||||
'visited_regions': []
|
||||
}
|
||||
|
||||
# Export Visited Cities
|
||||
for visited_city in user.visitedcity_set.all():
|
||||
export_data['visited_cities'].append({
|
||||
'city': visited_city.city.id,
|
||||
})
|
||||
|
||||
# Export Visited Regions
|
||||
for visited_region in user.visitedregion_set.all():
|
||||
export_data['visited_regions'].append({
|
||||
'region': visited_region.region.id,
|
||||
})
|
||||
|
||||
# Export Categories
|
||||
for category in user.category_set.all():
|
||||
export_data['categories'].append({
|
||||
'name': category.name,
|
||||
'display_name': category.display_name,
|
||||
'icon': category.icon,
|
||||
})
|
||||
|
||||
# Export Collections
|
||||
for idx, collection in enumerate(user.collection_set.all()):
|
||||
export_data['collections'].append({
|
||||
'export_id': idx, # Add unique identifier for this export
|
||||
'name': collection.name,
|
||||
'description': collection.description,
|
||||
'is_public': collection.is_public,
|
||||
'start_date': collection.start_date.isoformat() if collection.start_date else None,
|
||||
'end_date': collection.end_date.isoformat() if collection.end_date else None,
|
||||
'is_archived': collection.is_archived,
|
||||
'link': collection.link,
|
||||
'shared_with_user_ids': [str(uuid) for uuid in collection.shared_with.values_list('uuid', flat=True)]
|
||||
})
|
||||
|
||||
# Create collection name to export_id mapping
|
||||
collection_name_to_id = {col.name: idx for idx, col in enumerate(user.collection_set.all())}
|
||||
|
||||
# Export locations with related data
|
||||
for idx, location in enumerate(user.location_set.all()):
|
||||
location_data = {
|
||||
'export_id': idx, # Add unique identifier for this export
|
||||
'name': location.name,
|
||||
'location': location.location,
|
||||
'tags': location.tags,
|
||||
'description': location.description,
|
||||
'rating': location.rating,
|
||||
'link': location.link,
|
||||
'is_public': location.is_public,
|
||||
'longitude': str(location.longitude) if location.longitude else None,
|
||||
'latitude': str(location.latitude) if location.latitude else None,
|
||||
'city': location.city_id,
|
||||
'region': location.region_id,
|
||||
'country': location.country_id,
|
||||
'category_name': location.category.name if location.category else None,
|
||||
'collection_export_ids': [collection_name_to_id[col_name] for col_name in location.collections.values_list('name', flat=True) if col_name in collection_name_to_id],
|
||||
'visits': [],
|
||||
'images': [],
|
||||
'attachments': []
|
||||
}
|
||||
|
||||
# Add visits
|
||||
for visit in location.visits.all():
|
||||
location_data['visits'].append({
|
||||
'start_date': visit.start_date.isoformat() if visit.start_date else None,
|
||||
'end_date': visit.end_date.isoformat() if visit.end_date else None,
|
||||
'timezone': visit.timezone,
|
||||
'notes': visit.notes
|
||||
})
|
||||
|
||||
# Add images
|
||||
for image in location.images.all():
|
||||
image_data = {
|
||||
'immich_id': image.immich_id,
|
||||
'is_primary': image.is_primary,
|
||||
'filename': None
|
||||
}
|
||||
if image.image:
|
||||
image_data['filename'] = image.image.name.split('/')[-1]
|
||||
location_data['images'].append(image_data)
|
||||
|
||||
# Add attachments
|
||||
for attachment in location.attachments.all():
|
||||
attachment_data = {
|
||||
'name': attachment.name,
|
||||
'filename': None
|
||||
}
|
||||
if attachment.file:
|
||||
attachment_data['filename'] = attachment.file.name.split('/')[-1]
|
||||
location_data['attachments'].append(attachment_data)
|
||||
|
||||
export_data['locations'].append(location_data)
|
||||
|
||||
# Export Transportation
|
||||
for transport in user.transportation_set.all():
|
||||
collection_export_id = None
|
||||
if transport.collection:
|
||||
collection_export_id = collection_name_to_id.get(transport.collection.name)
|
||||
|
||||
export_data['transportation'].append({
|
||||
'type': transport.type,
|
||||
'name': transport.name,
|
||||
'description': transport.description,
|
||||
'rating': transport.rating,
|
||||
'link': transport.link,
|
||||
'date': transport.date.isoformat() if transport.date else None,
|
||||
'end_date': transport.end_date.isoformat() if transport.end_date else None,
|
||||
'start_timezone': transport.start_timezone,
|
||||
'end_timezone': transport.end_timezone,
|
||||
'flight_number': transport.flight_number,
|
||||
'from_location': transport.from_location,
|
||||
'origin_latitude': str(transport.origin_latitude) if transport.origin_latitude else None,
|
||||
'origin_longitude': str(transport.origin_longitude) if transport.origin_longitude else None,
|
||||
'destination_latitude': str(transport.destination_latitude) if transport.destination_latitude else None,
|
||||
'destination_longitude': str(transport.destination_longitude) if transport.destination_longitude else None,
|
||||
'to_location': transport.to_location,
|
||||
'is_public': transport.is_public,
|
||||
'collection_export_id': collection_export_id
|
||||
})
|
||||
|
||||
# Export Notes
|
||||
for note in user.note_set.all():
|
||||
collection_export_id = None
|
||||
if note.collection:
|
||||
collection_export_id = collection_name_to_id.get(note.collection.name)
|
||||
|
||||
export_data['notes'].append({
|
||||
'name': note.name,
|
||||
'content': note.content,
|
||||
'links': note.links,
|
||||
'date': note.date.isoformat() if note.date else None,
|
||||
'is_public': note.is_public,
|
||||
'collection_export_id': collection_export_id
|
||||
})
|
||||
|
||||
# Export Checklists
|
||||
for checklist in user.checklist_set.all():
|
||||
collection_export_id = None
|
||||
if checklist.collection:
|
||||
collection_export_id = collection_name_to_id.get(checklist.collection.name)
|
||||
|
||||
checklist_data = {
|
||||
'name': checklist.name,
|
||||
'date': checklist.date.isoformat() if checklist.date else None,
|
||||
'is_public': checklist.is_public,
|
||||
'collection_export_id': collection_export_id,
|
||||
'items': []
|
||||
}
|
||||
|
||||
# Add checklist items
|
||||
for item in checklist.checklistitem_set.all():
|
||||
checklist_data['items'].append({
|
||||
'name': item.name,
|
||||
'is_checked': item.is_checked
|
||||
})
|
||||
|
||||
export_data['checklists'].append(checklist_data)
|
||||
|
||||
# Export Lodging
|
||||
for lodging in user.lodging_set.all():
|
||||
collection_export_id = None
|
||||
if lodging.collection:
|
||||
collection_export_id = collection_name_to_id.get(lodging.collection.name)
|
||||
|
||||
export_data['lodging'].append({
|
||||
'name': lodging.name,
|
||||
'type': lodging.type,
|
||||
'description': lodging.description,
|
||||
'rating': lodging.rating,
|
||||
'link': lodging.link,
|
||||
'check_in': lodging.check_in.isoformat() if lodging.check_in else None,
|
||||
'check_out': lodging.check_out.isoformat() if lodging.check_out else None,
|
||||
'timezone': lodging.timezone,
|
||||
'reservation_number': lodging.reservation_number,
|
||||
'price': str(lodging.price) if lodging.price else None,
|
||||
'latitude': str(lodging.latitude) if lodging.latitude else None,
|
||||
'longitude': str(lodging.longitude) if lodging.longitude else None,
|
||||
'location': lodging.location,
|
||||
'is_public': lodging.is_public,
|
||||
'collection_export_id': collection_export_id
|
||||
})
|
||||
|
||||
# Create ZIP file
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as tmp_file:
|
||||
with zipfile.ZipFile(tmp_file.name, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
# Add JSON data
|
||||
zip_file.writestr('data.json', json.dumps(export_data, indent=2))
|
||||
|
||||
# Add images and attachments
|
||||
files_added = set()
|
||||
|
||||
for location in user.location_set.all():
|
||||
# Add images
|
||||
for image in location.images.all():
|
||||
if image.image and image.image.name not in files_added:
|
||||
try:
|
||||
image_content = default_storage.open(image.image.name).read()
|
||||
filename = image.image.name.split('/')[-1]
|
||||
zip_file.writestr(f'images/{filename}', image_content)
|
||||
files_added.add(image.image.name)
|
||||
except Exception as e:
|
||||
print(f"Error adding image {image.image.name}: {e}")
|
||||
|
||||
# Add attachments
|
||||
for attachment in location.attachments.all():
|
||||
if attachment.file and attachment.file.name not in files_added:
|
||||
try:
|
||||
file_content = default_storage.open(attachment.file.name).read()
|
||||
filename = attachment.file.name.split('/')[-1]
|
||||
zip_file.writestr(f'attachments/{filename}', file_content)
|
||||
files_added.add(attachment.file.name)
|
||||
except Exception as e:
|
||||
print(f"Error adding attachment {attachment.file.name}: {e}")
|
||||
|
||||
# Return ZIP file as response
|
||||
with open(tmp_file.name, 'rb') as zip_file:
|
||||
response = HttpResponse(zip_file.read(), content_type='application/zip')
|
||||
filename = f"adventurelog_backup_{user.username}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
|
||||
# Clean up
|
||||
os.unlink(tmp_file.name)
|
||||
return response
|
||||
|
||||
@action(
|
||||
detail=False,
|
||||
methods=['post'],
|
||||
parser_classes=[MultiPartParser],
|
||||
url_path='import', # changes the URL path to /import
|
||||
url_name='import' # changes the reverse name to 'import'
|
||||
)
|
||||
def import_data(self, request):
|
||||
"""
|
||||
Import data from a ZIP backup file
|
||||
"""
|
||||
if 'file' not in request.FILES:
|
||||
return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if 'confirm' not in request.data or request.data['confirm'] != 'yes':
|
||||
return Response({'error': 'Confirmation required to proceed with import'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
backup_file = request.FILES['file']
|
||||
user = request.user
|
||||
|
||||
# Save file temporarily
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as tmp_file:
|
||||
for chunk in backup_file.chunks():
|
||||
tmp_file.write(chunk)
|
||||
tmp_file_path = tmp_file.name
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(tmp_file_path, 'r') as zip_file:
|
||||
# Validate backup structure
|
||||
if 'data.json' not in zip_file.namelist():
|
||||
return Response({'error': 'Invalid backup file - missing data.json'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Load data
|
||||
backup_data = json.loads(zip_file.read('data.json').decode('utf-8'))
|
||||
|
||||
# Import with transaction
|
||||
with transaction.atomic():
|
||||
# Clear existing data first
|
||||
self._clear_user_data(user)
|
||||
summary = self._import_data(backup_data, zip_file, user)
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Data imported successfully',
|
||||
'summary': summary
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return Response({'error': 'Invalid JSON in backup file'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception:
|
||||
import logging
|
||||
logging.error("Import failed", exc_info=True)
|
||||
return Response({'error': 'An internal error occurred during import'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
finally:
|
||||
os.unlink(tmp_file_path)
|
||||
|
||||
def _clear_user_data(self, user):
|
||||
"""Clear all existing user data before import"""
|
||||
# Delete in reverse order of dependencies
|
||||
user.checklistitem_set.all().delete()
|
||||
user.checklist_set.all().delete()
|
||||
user.note_set.all().delete()
|
||||
user.transportation_set.all().delete()
|
||||
user.lodging_set.all().delete()
|
||||
|
||||
# Delete location-related data
|
||||
user.locationimage_set.all().delete()
|
||||
user.attachment_set.all().delete()
|
||||
# Visits are deleted via cascade when locations are deleted
|
||||
user.location_set.all().delete()
|
||||
|
||||
# Delete collections and categories last
|
||||
user.collection_set.all().delete()
|
||||
user.category_set.all().delete()
|
||||
|
||||
# Clear visited cities and regions
|
||||
user.visitedcity_set.all().delete()
|
||||
user.visitedregion_set.all().delete()
|
||||
|
||||
def _import_data(self, backup_data, zip_file, user):
|
||||
"""Import backup data and return summary"""
|
||||
# Track mappings and counts
|
||||
category_map = {}
|
||||
collection_map = {} # Map export_id to actual collection object
|
||||
summary = {
|
||||
'categories': 0, 'collections': 0, 'locations': 0,
|
||||
'transportation': 0, 'notes': 0, 'checklists': 0,
|
||||
'checklist_items': 0, 'lodging': 0, 'images': 0, 'attachments': 0, 'visited_cities': 0, 'visited_regions': 0
|
||||
}
|
||||
|
||||
# Import Visited Cities
|
||||
for city_data in backup_data.get('visited_cities', []):
|
||||
try:
|
||||
city_obj = City.objects.get(id=city_data['city'])
|
||||
VisitedCity.objects.create(user=user, city=city_obj)
|
||||
summary['visited_cities'] += 1
|
||||
except City.DoesNotExist:
|
||||
# If city does not exist, we can skip or log it
|
||||
pass
|
||||
|
||||
# Import Visited Regions
|
||||
for region_data in backup_data.get('visited_regions', []):
|
||||
try:
|
||||
region_obj = Region.objects.get(id=region_data['region'])
|
||||
VisitedRegion.objects.create(user=user, region=region_obj)
|
||||
summary['visited_regions'] += 1
|
||||
except Region.DoesNotExist:
|
||||
# If region does not exist, we can skip or log it
|
||||
pass
|
||||
|
||||
# Import Categories
|
||||
for cat_data in backup_data.get('categories', []):
|
||||
category = Category.objects.create(
|
||||
user=user,
|
||||
name=cat_data['name'],
|
||||
display_name=cat_data['display_name'],
|
||||
icon=cat_data.get('icon', '🌍')
|
||||
)
|
||||
category_map[cat_data['name']] = category
|
||||
summary['categories'] += 1
|
||||
|
||||
# Import Collections
|
||||
for col_data in backup_data.get('collections', []):
|
||||
collection = Collection.objects.create(
|
||||
user=user,
|
||||
name=col_data['name'],
|
||||
description=col_data.get('description', ''),
|
||||
is_public=col_data.get('is_public', False),
|
||||
start_date=col_data.get('start_date'),
|
||||
end_date=col_data.get('end_date'),
|
||||
is_archived=col_data.get('is_archived', False),
|
||||
link=col_data.get('link')
|
||||
)
|
||||
collection_map[col_data['export_id']] = collection
|
||||
summary['collections'] += 1
|
||||
|
||||
# Handle shared users
|
||||
for uuid in col_data.get('shared_with_user_ids', []):
|
||||
try:
|
||||
shared_user = User.objects.get(uuid=uuid)
|
||||
if shared_user.public_profile:
|
||||
collection.shared_with.add(shared_user)
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Import Locations
|
||||
for adv_data in backup_data.get('locations', []):
|
||||
|
||||
city = None
|
||||
if adv_data.get('city'):
|
||||
try:
|
||||
city = City.objects.get(id=adv_data['city'])
|
||||
except City.DoesNotExist:
|
||||
city = None
|
||||
|
||||
region = None
|
||||
if adv_data.get('region'):
|
||||
try:
|
||||
region = Region.objects.get(id=adv_data['region'])
|
||||
except Region.DoesNotExist:
|
||||
region = None
|
||||
|
||||
country = None
|
||||
if adv_data.get('country'):
|
||||
try:
|
||||
country = Country.objects.get(id=adv_data['country'])
|
||||
except Country.DoesNotExist:
|
||||
country = None
|
||||
|
||||
location = Location(
|
||||
user=user,
|
||||
name=adv_data['name'],
|
||||
location=adv_data.get('location'),
|
||||
tags=adv_data.get('tags', []),
|
||||
description=adv_data.get('description'),
|
||||
rating=adv_data.get('rating'),
|
||||
link=adv_data.get('link'),
|
||||
is_public=adv_data.get('is_public', False),
|
||||
longitude=adv_data.get('longitude'),
|
||||
latitude=adv_data.get('latitude'),
|
||||
city=city,
|
||||
region=region,
|
||||
country=country,
|
||||
category=category_map.get(adv_data.get('category_name'))
|
||||
)
|
||||
location.save(_skip_geocode=True) # Skip geocoding for now
|
||||
|
||||
# Add to collections using export_ids
|
||||
for collection_export_id in adv_data.get('collection_export_ids', []):
|
||||
if collection_export_id in collection_map:
|
||||
location.collections.add(collection_map[collection_export_id])
|
||||
|
||||
# Import visits
|
||||
for visit_data in adv_data.get('visits', []):
|
||||
Visit.objects.create(
|
||||
location=location,
|
||||
start_date=visit_data.get('start_date'),
|
||||
end_date=visit_data.get('end_date'),
|
||||
timezone=visit_data.get('timezone'),
|
||||
notes=visit_data.get('notes')
|
||||
)
|
||||
|
||||
# Import images
|
||||
for img_data in adv_data.get('images', []):
|
||||
immich_id = img_data.get('immich_id')
|
||||
if immich_id:
|
||||
LocationImage.objects.create(
|
||||
user=user,
|
||||
location=location,
|
||||
immich_id=immich_id,
|
||||
is_primary=img_data.get('is_primary', False)
|
||||
)
|
||||
summary['images'] += 1
|
||||
else:
|
||||
filename = img_data.get('filename')
|
||||
if filename:
|
||||
try:
|
||||
img_content = zip_file.read(f'images/{filename}')
|
||||
img_file = ContentFile(img_content, name=filename)
|
||||
LocationImage.objects.create(
|
||||
user=user,
|
||||
location=location,
|
||||
image=img_file,
|
||||
is_primary=img_data.get('is_primary', False)
|
||||
)
|
||||
summary['images'] += 1
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Import attachments
|
||||
for att_data in adv_data.get('attachments', []):
|
||||
filename = att_data.get('filename')
|
||||
if filename:
|
||||
try:
|
||||
att_content = zip_file.read(f'attachments/{filename}')
|
||||
att_file = ContentFile(att_content, name=filename)
|
||||
Attachment.objects.create(
|
||||
user=user,
|
||||
location=location,
|
||||
file=att_file,
|
||||
name=att_data.get('name')
|
||||
)
|
||||
summary['attachments'] += 1
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
summary['locations'] += 1
|
||||
|
||||
# Import Transportation
|
||||
for trans_data in backup_data.get('transportation', []):
|
||||
collection = None
|
||||
if trans_data.get('collection_export_id') is not None:
|
||||
collection = collection_map.get(trans_data['collection_export_id'])
|
||||
|
||||
Transportation.objects.create(
|
||||
user=user,
|
||||
type=trans_data['type'],
|
||||
name=trans_data['name'],
|
||||
description=trans_data.get('description'),
|
||||
rating=trans_data.get('rating'),
|
||||
link=trans_data.get('link'),
|
||||
date=trans_data.get('date'),
|
||||
end_date=trans_data.get('end_date'),
|
||||
start_timezone=trans_data.get('start_timezone'),
|
||||
end_timezone=trans_data.get('end_timezone'),
|
||||
flight_number=trans_data.get('flight_number'),
|
||||
from_location=trans_data.get('from_location'),
|
||||
origin_latitude=trans_data.get('origin_latitude'),
|
||||
origin_longitude=trans_data.get('origin_longitude'),
|
||||
destination_latitude=trans_data.get('destination_latitude'),
|
||||
destination_longitude=trans_data.get('destination_longitude'),
|
||||
to_location=trans_data.get('to_location'),
|
||||
is_public=trans_data.get('is_public', False),
|
||||
collection=collection
|
||||
)
|
||||
summary['transportation'] += 1
|
||||
|
||||
# Import Notes
|
||||
for note_data in backup_data.get('notes', []):
|
||||
collection = None
|
||||
if note_data.get('collection_export_id') is not None:
|
||||
collection = collection_map.get(note_data['collection_export_id'])
|
||||
|
||||
Note.objects.create(
|
||||
user=user,
|
||||
name=note_data['name'],
|
||||
content=note_data.get('content'),
|
||||
links=note_data.get('links', []),
|
||||
date=note_data.get('date'),
|
||||
is_public=note_data.get('is_public', False),
|
||||
collection=collection
|
||||
)
|
||||
summary['notes'] += 1
|
||||
|
||||
# Import Checklists
|
||||
for check_data in backup_data.get('checklists', []):
|
||||
collection = None
|
||||
if check_data.get('collection_export_id') is not None:
|
||||
collection = collection_map.get(check_data['collection_export_id'])
|
||||
|
||||
checklist = Checklist.objects.create(
|
||||
user=user,
|
||||
name=check_data['name'],
|
||||
date=check_data.get('date'),
|
||||
is_public=check_data.get('is_public', False),
|
||||
collection=collection
|
||||
)
|
||||
|
||||
# Import checklist items
|
||||
for item_data in check_data.get('items', []):
|
||||
ChecklistItem.objects.create(
|
||||
user=user,
|
||||
checklist=checklist,
|
||||
name=item_data['name'],
|
||||
is_checked=item_data.get('is_checked', False)
|
||||
)
|
||||
summary['checklist_items'] += 1
|
||||
|
||||
summary['checklists'] += 1
|
||||
|
||||
# Import Lodging
|
||||
for lodg_data in backup_data.get('lodging', []):
|
||||
collection = None
|
||||
if lodg_data.get('collection_export_id') is not None:
|
||||
collection = collection_map.get(lodg_data['collection_export_id'])
|
||||
|
||||
Lodging.objects.create(
|
||||
user=user,
|
||||
name=lodg_data['name'],
|
||||
type=lodg_data.get('type', 'other'),
|
||||
description=lodg_data.get('description'),
|
||||
rating=lodg_data.get('rating'),
|
||||
link=lodg_data.get('link'),
|
||||
check_in=lodg_data.get('check_in'),
|
||||
check_out=lodg_data.get('check_out'),
|
||||
timezone=lodg_data.get('timezone'),
|
||||
reservation_number=lodg_data.get('reservation_number'),
|
||||
price=lodg_data.get('price'),
|
||||
latitude=lodg_data.get('latitude'),
|
||||
longitude=lodg_data.get('longitude'),
|
||||
location=lodg_data.get('location'),
|
||||
is_public=lodg_data.get('is_public', False),
|
||||
collection=collection
|
||||
)
|
||||
summary['lodging'] += 1
|
||||
|
||||
return summary
|
|
@ -297,7 +297,7 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
|||
is_authorized = True
|
||||
|
||||
# Level 3: Owner access
|
||||
elif request.user.is_authenticated and request.user.id == owner_id:
|
||||
elif request.user.is_authenticated and request.user == owner_id:
|
||||
is_authorized = True
|
||||
|
||||
# Level 4: Shared collection access - check if user has access to any collection
|
||||
|
@ -314,7 +314,7 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
|||
}, status=status.HTTP_403_FORBIDDEN)
|
||||
else:
|
||||
# No LocationImage exists; allow only the integration owner
|
||||
if not request.user.is_authenticated or request.user.id != owner_id:
|
||||
if not request.user.is_authenticated or request.user != owner_id:
|
||||
return Response({
|
||||
'message': 'Image is not linked to any location and you are not the owner.',
|
||||
'error': True,
|
||||
|
|
|
@ -326,6 +326,9 @@ LOGGING = {
|
|||
|
||||
# ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app')
|
||||
|
||||
# Major release version of AdventureLog, not including the patch version date.
|
||||
ADVENTURELOG_RELEASE_VERSION = 'v0.10.0'
|
||||
|
||||
# https://github.com/dr5hn/countries-states-cities-database/tags
|
||||
COUNTRY_REGION_JSON_VERSION = 'v2.6'
|
||||
|
||||
|
|
|
@ -33,146 +33,201 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<dialog id="about_modal" class="modal backdrop-blur-md bg-opacity-70">
|
||||
<dialog id="about_modal" class="modal backdrop-blur-sm">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<div
|
||||
class="modal-box rounded-xl shadow-lg backdrop-blur-lg bg-white/80 dark:bg-gray-900/80 transition-transform duration-300 ease-out transform scale-100"
|
||||
class="modal-box w-11/12 max-w-2xl bg-gradient-to-br from-base-100 via-base-100 to-base-200 border border-base-300 shadow-2xl"
|
||||
role="dialog"
|
||||
on:keydown={handleKeydown}
|
||||
tabindex="0"
|
||||
>
|
||||
<!-- Branding -->
|
||||
<div class="text-center">
|
||||
<h3
|
||||
class="text-2xl font-extrabold text-gray-800 dark:text-white flex items-center justify-center"
|
||||
>
|
||||
{$t('about.about')} AdventureLog
|
||||
<img src="/favicon.png" alt="Map Logo" class="w-12 h-12 ml-3 inline-block" />
|
||||
</h3>
|
||||
<p class="mt-2 text-gray-500 dark:text-gray-300 text-sm">
|
||||
AdventureLog
|
||||
<a
|
||||
href={versionChangelog}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary hover:underline"
|
||||
>
|
||||
{appVersion}
|
||||
</a>
|
||||
</p>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-primary/10 rounded-lg">
|
||||
<img src="/favicon.png" alt="AdventureLog" class="w-12 h-12" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-primary">
|
||||
{$t('about.about')} AdventureLog
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-ghost btn-sm btn-square" on:click={close}>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Links and Details -->
|
||||
<div class="mt-4 text-center">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
© {copyrightYear}
|
||||
<a
|
||||
href="https://seanmorley.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary hover:underline"
|
||||
>
|
||||
Sean Morley
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{$t('about.license')}</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<a
|
||||
href="https://github.com/seanmorley15/AdventureLog"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary hover:underline"
|
||||
>
|
||||
{$t('about.source_code')}
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{$t('about.message')}</p>
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="space-y-4">
|
||||
<!-- Version & Developer Info -->
|
||||
<div class="card bg-base-200/30 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="text-sm text-base-content/60">{$t('about.version')}</div>
|
||||
<div class="text-lg font-bold text-primary">{appVersion}</div>
|
||||
<a
|
||||
href={versionChangelog}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm link link-primary"
|
||||
>
|
||||
{$t('about.view_changelog')} →
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-base-content/60">{$t('about.developer')}</div>
|
||||
<a
|
||||
href="https://seanmorley.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-lg font-semibold link link-primary"
|
||||
>
|
||||
Sean Morley
|
||||
</a>
|
||||
<div class="text-sm text-base-content/60">{$t('about.message')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="my-6 border-t border-gray-200 dark:border-gray-700"></div>
|
||||
<!-- Map Services -->
|
||||
<div class="card bg-base-200/30 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="font-bold text-primary mb-3 flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{$t('about.attributions')}
|
||||
</h3>
|
||||
{#if integrations && integrations?.google_maps}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-base-content/60">{$t('about.nominatim_1')}</span>
|
||||
<a
|
||||
href="https://developers.google.com/maps/terms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link link-primary font-semibold"
|
||||
>
|
||||
Google Maps Platform
|
||||
</a>
|
||||
</div>
|
||||
{:else if integrations && !integrations?.google_maps}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-base-content/60">{$t('about.nominatim_1')}</span>
|
||||
<a
|
||||
href="https://operations.osmfoundation.org/policies/nominatim/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link link-primary font-semibold"
|
||||
>
|
||||
OpenStreetMap Nominatim
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-base-content/60">{$t('about.generic_attributions')}</div>
|
||||
{/if}
|
||||
<p class="text-sm text-base-content/60">{$t('about.other_attributions')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liscense info -->
|
||||
<div class="card bg-base-200/30 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="font-bold text-primary mb-3 flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
{$t('about.license_info')}
|
||||
</h3>
|
||||
<p class="text-sm text-base-content/60 mb-2">
|
||||
© {copyrightYear}
|
||||
<a
|
||||
href="https://seanmorley.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link link-primary"
|
||||
>
|
||||
Sean Morley
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-sm text-base-content/60">
|
||||
{$t('about.license')}
|
||||
</p>
|
||||
|
||||
<!-- OSS Acknowledgments -->
|
||||
<div class="text-left">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-white">
|
||||
{$t('about.oss_attributions')}
|
||||
</h3>
|
||||
{#if integrations && integrations?.google_maps}
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
{$t('about.nominatim_1')}
|
||||
<a
|
||||
href="https://developers.google.com/maps/terms"
|
||||
href="https://github.com/seanmorley15/AdventureLog/blob/main/LICENSE"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary hover:underline"
|
||||
class="link link-primary mt-2"
|
||||
>
|
||||
Google Maps
|
||||
{$t('about.view_license')} →
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
{:else if integrations && !integrations?.google_maps}
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
{$t('about.nominatim_1')}
|
||||
<a
|
||||
href="https://operations.osmfoundation.org/policies/nominatim/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary hover:underline"
|
||||
>
|
||||
OpenStreetMap
|
||||
</a>
|
||||
. {$t('about.nominatim_2')}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
{$t('about.generic_attributions')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{$t('about.other_attributions')}</p>
|
||||
<!-- Links -->
|
||||
<div class="card bg-base-200/30 border border-base-300">
|
||||
<div class="card-body p-4">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a
|
||||
href="https://github.com/seanmorley15/AdventureLog"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-outline btn-sm"
|
||||
>
|
||||
GitHub →
|
||||
</a>
|
||||
<a
|
||||
href="https://seanmorley.com/sponsor"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-outline btn-sm"
|
||||
>
|
||||
{$t('about.sponsor')} →
|
||||
</a>
|
||||
<!-- documentation -->
|
||||
<a
|
||||
href="https://adventurelog.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-outline btn-sm"
|
||||
>
|
||||
{$t('navbar.documentation')} →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Close Button -->
|
||||
<div class="flex justify-center mt-6">
|
||||
<button
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-primary rounded-full shadow-md hover:shadow-lg hover:scale-105 transform transition"
|
||||
on:click={close}
|
||||
>
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-between mt-6 pt-4 border-t border-base-300">
|
||||
<div class="text-sm text-base-content/60">
|
||||
{$t('about.thank_you')}
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" on:click={close}>
|
||||
{$t('about.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
.modal-box {
|
||||
max-width: 600px;
|
||||
padding: 2rem;
|
||||
animation: slideUp 0.4s ease-out;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
export let adventures: Location[] = [];
|
||||
|
||||
let currentSlide = 0;
|
||||
let image_url: string | null = null;
|
||||
let showImageModal = false;
|
||||
let modalInitialIndex = 0;
|
||||
|
||||
$: adventure_images = adventures.flatMap((adventure) =>
|
||||
adventure.images.map((image) => ({
|
||||
|
@ -42,13 +43,22 @@
|
|||
currentSlide = currentSlide - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function openImageModal(initialIndex: number = currentSlide) {
|
||||
modalInitialIndex = initialIndex;
|
||||
showImageModal = true;
|
||||
}
|
||||
|
||||
function closeImageModal() {
|
||||
showImageModal = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if image_url}
|
||||
{#if showImageModal && adventure_images.length > 0}
|
||||
<ImageDisplayModal
|
||||
adventure={adventure_images[currentSlide].adventure}
|
||||
image={image_url}
|
||||
on:close={() => (image_url = null)}
|
||||
images={adventure_images}
|
||||
initialIndex={modalInitialIndex}
|
||||
on:close={closeImageModal}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -61,14 +71,46 @@
|
|||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a
|
||||
on:click|stopPropagation={() => (image_url = adventure_images[currentSlide].image)}
|
||||
class="cursor-pointer"
|
||||
on:click|stopPropagation={() => openImageModal(currentSlide)}
|
||||
class="cursor-pointer relative group"
|
||||
>
|
||||
<img
|
||||
src={adventure_images[currentSlide].image}
|
||||
class="w-full h-48 object-cover"
|
||||
class="w-full h-48 object-cover transition-all group-hover:brightness-110"
|
||||
alt={adventure_images[currentSlide].adventure.name}
|
||||
/>
|
||||
|
||||
<!-- Overlay indicator for multiple images -->
|
||||
<!-- {#if adventure_images.length > 1}
|
||||
<div
|
||||
class="absolute top-3 right-3 bg-black/60 text-white px-2 py-1 rounded-lg text-xs font-medium"
|
||||
>
|
||||
{currentSlide + 1} / {adventure_images.length}
|
||||
</div>
|
||||
{/if} -->
|
||||
|
||||
<!-- Click to expand hint -->
|
||||
<!-- <div
|
||||
class="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-all flex items-center justify-center"
|
||||
>
|
||||
<div
|
||||
class="opacity-0 group-hover:opacity-100 transition-all bg-white/90 rounded-full p-2"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-gray-800"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div> -->
|
||||
</a>
|
||||
|
||||
{#if adventure_images.length > 1}
|
||||
|
@ -76,8 +118,18 @@
|
|||
{#if currentSlide > 0}
|
||||
<button
|
||||
on:click|stopPropagation={() => changeSlide('prev')}
|
||||
class="btn btn-circle btn-sm ml-2 pointer-events-auto">❮</button
|
||||
class="btn btn-circle btn-sm mr-2 pointer-events-auto bg-neutral border-none text-neutral-content shadow-lg"
|
||||
aria-label="Previous image"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="w-12"></div>
|
||||
{/if}
|
||||
|
@ -85,17 +137,43 @@
|
|||
{#if currentSlide < adventure_images.length - 1}
|
||||
<button
|
||||
on:click|stopPropagation={() => changeSlide('next')}
|
||||
class="btn btn-circle mr-2 btn-sm pointer-events-auto">❯</button
|
||||
class="btn btn-circle btn-sm mr-2 pointer-events-auto bg-neutral border-none text-neutral-content shadow-lg"
|
||||
aria-label="Next image"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="w-12"></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Dot indicators at bottom -->
|
||||
<!-- {#if adventure_images.length > 1}
|
||||
<div class="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2">
|
||||
{#each adventure_images as _, index}
|
||||
<button
|
||||
on:click|stopPropagation={() => (currentSlide = index)}
|
||||
class="w-2 h-2 rounded-full transition-all pointer-events-auto {index ===
|
||||
currentSlide
|
||||
? 'bg-white shadow-lg'
|
||||
: 'bg-white/50 hover:bg-white/80'}"
|
||||
aria-label="Go to image {index + 1}"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if} -->
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- add a figure with a gradient instead - -->
|
||||
<!-- add a figure with a gradient instead -->
|
||||
<div class="w-full h-48 bg-gradient-to-r from-success via-base to-primary relative">
|
||||
<!-- subtle button bottom left text
|
||||
<div
|
||||
|
|
|
@ -5,14 +5,21 @@
|
|||
let modal: HTMLDialogElement;
|
||||
import type { Location } from '$lib/types';
|
||||
|
||||
export let image: string;
|
||||
export let adventure: Location | null = null;
|
||||
export let images: { image: string; adventure: Location | null }[] = [];
|
||||
export let initialIndex: number = 0;
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let currentIndex = initialIndex;
|
||||
let currentImage = images[currentIndex]?.image || '';
|
||||
let currentAdventure = images[currentIndex]?.adventure || null;
|
||||
|
||||
onMount(() => {
|
||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
// Set initial values
|
||||
updateCurrentSlide(initialIndex);
|
||||
});
|
||||
|
||||
function close() {
|
||||
|
@ -25,6 +32,10 @@
|
|||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
} else if (event.key === 'ArrowLeft') {
|
||||
previousSlide();
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
nextSlide();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,44 +44,228 @@
|
|||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function updateCurrentSlide(index: number) {
|
||||
currentIndex = index;
|
||||
currentImage = images[currentIndex]?.image || '';
|
||||
currentAdventure = images[currentIndex]?.adventure || null;
|
||||
}
|
||||
|
||||
function nextSlide() {
|
||||
if (images.length > 0) {
|
||||
const nextIndex = (currentIndex + 1) % images.length;
|
||||
updateCurrentSlide(nextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function previousSlide() {
|
||||
if (images.length > 0) {
|
||||
const prevIndex = (currentIndex - 1 + images.length) % images.length;
|
||||
updateCurrentSlide(prevIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function goToSlide(index: number) {
|
||||
updateCurrentSlide(index);
|
||||
}
|
||||
|
||||
// Reactive statement to handle prop changes
|
||||
$: if (images.length > 0 && currentIndex >= images.length) {
|
||||
updateCurrentSlide(0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<dialog id="my_modal_1" class="modal" on:click={handleClickOutside}>
|
||||
<dialog id="my_modal_1" class="modal backdrop-blur-sm" on:click={handleClickOutside}>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div class="modal-box w-11/12 max-w-5xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
{#if adventure}
|
||||
<div class="modal-header flex justify-between items-center mb-4">
|
||||
<h3 class="font-bold text-2xl">{adventure.name}</h3>
|
||||
<button class="btn btn-circle btn-neutral" on:click={close}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="modal-box w-11/12 max-w-6xl bg-gradient-to-br from-base-100 via-base-100 to-base-200 border border-base-300 shadow-2xl"
|
||||
role="dialog"
|
||||
on:keydown={handleKeydown}
|
||||
tabindex="0"
|
||||
>
|
||||
{#if currentAdventure && currentImage}
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex justify-center items-center"
|
||||
style="display: flex; justify-content: center; align-items: center;"
|
||||
class="top-0 z-10 bg-base-100/90 backdrop-blur-lg border-b border-base-300 -mx-6 -mt-6 px-6 py-4 mb-6"
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={adventure.name}
|
||||
style="max-width: 100%; max-height: 75vh; object-fit: contain;"
|
||||
/>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-primary/10 rounded-xl">
|
||||
<svg
|
||||
class="w-6 h-6 text-primary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-primary">
|
||||
{currentAdventure.name}
|
||||
</h1>
|
||||
{#if images.length > 1}
|
||||
<p class="text-sm text-base-content/60">
|
||||
{currentIndex + 1} of {images.length}
|
||||
{$t('adventures.images')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation indicators for multiple images -->
|
||||
{#if images.length > 1}
|
||||
<div class="hidden md:flex items-center gap-2">
|
||||
<div class="flex gap-1">
|
||||
{#each images as _, index}
|
||||
<button
|
||||
class="w-2 h-2 rounded-full transition-all {index === currentIndex
|
||||
? 'bg-primary'
|
||||
: 'bg-base-300 hover:bg-base-400'}"
|
||||
on:click={() => goToSlide(index)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Close Button -->
|
||||
<button class="btn btn-ghost btn-square" on:click={close}>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Display Area -->
|
||||
<div class="relative h-[75vh] flex justify-center items-center max-w-full">
|
||||
<!-- Previous Button -->
|
||||
{#if images.length > 1}
|
||||
<button
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 z-20 btn btn-circle btn-primary/80 hover:btn-primary"
|
||||
on:click={previousSlide}
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Main Image -->
|
||||
<div class="flex justify-center items-center max-w-full">
|
||||
<img
|
||||
src={currentImage}
|
||||
alt={currentAdventure.name}
|
||||
class="max-w-full max-h-[75vh] object-contain rounded-lg shadow-lg"
|
||||
style="max-width: 100%; max-height: 75vh; object-fit: contain;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Next Button -->
|
||||
{#if images.length > 1}
|
||||
<button
|
||||
class="absolute right-4 top-1/2 -translate-y-1/2 z-20 btn btn-circle btn-primary/80 hover:btn-primary"
|
||||
on:click={nextSlide}
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Thumbnail Navigation (for multiple images) -->
|
||||
{#if images.length > 1}
|
||||
<div class="mt-6 px-2">
|
||||
<div class="flex gap-2 overflow-x-auto pb-2">
|
||||
{#each images as imageData, index}
|
||||
<button
|
||||
class="flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 transition-all {index ===
|
||||
currentIndex
|
||||
? 'border-primary shadow-lg'
|
||||
: 'border-base-300 hover:border-base-400'}"
|
||||
on:click={() => goToSlide(index)}
|
||||
>
|
||||
<img
|
||||
src={imageData.image}
|
||||
alt={imageData.adventure?.name || 'Image'}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Footer -->
|
||||
<div
|
||||
class="bottom-0 bg-base-100/90 backdrop-blur-lg border-t border-base-300 -mx-6 -mb-6 px-6 py-4 mt-6 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-base-content/60">
|
||||
{#if currentAdventure.location}
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
{currentAdventure.location}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
{#if images.length > 1}
|
||||
<div class="text-sm text-base-content/60">
|
||||
{$t('adventures.image_modal_navigate')}
|
||||
</div>
|
||||
{/if}
|
||||
<button class="btn btn-primary gap-2" on:click={close}>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
{$t('about.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { addToast } from '$lib/toasts';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
import { onMount } from 'svelte';
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
let url: string = '';
|
||||
|
||||
export let name: string | null = null;
|
||||
|
||||
let error = '';
|
||||
|
||||
onMount(() => {
|
||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchImage() {
|
||||
let res = await fetch(url);
|
||||
let data = await res.blob();
|
||||
if (!data) {
|
||||
error = 'No image found at that URL.';
|
||||
return;
|
||||
}
|
||||
let file = new File([data], 'image.jpg', { type: 'image/jpeg' });
|
||||
close();
|
||||
dispatch('image', { file });
|
||||
}
|
||||
|
||||
async function fetchWikiImage() {
|
||||
let res = await fetch(`/api/generate/img/?name=${name}`);
|
||||
let data = await res.json();
|
||||
if (data.source) {
|
||||
let imageUrl = data.source;
|
||||
let res = await fetch(imageUrl);
|
||||
let blob = await res.blob();
|
||||
let file = new File([blob], `${name}.jpg`, { type: 'image/jpeg' });
|
||||
close();
|
||||
dispatch('image', { file });
|
||||
} else {
|
||||
error = 'No image found for that Wikipedia article.';
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
dispatch('close');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog id="my_modal_1" class="modal">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-lg">Image Fetcher with URL</h3>
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
bind:value={url}
|
||||
placeholder="Enter a URL"
|
||||
/>
|
||||
<button class="btn btn-primary" on:click={fetchImage}>Submit</button>
|
||||
</form>
|
||||
|
||||
<h3 class="font-bold text-lg">Image Fetcher from Wikipedia</h3>
|
||||
<form>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
bind:value={name}
|
||||
placeholder="Enter a Wikipedia Article Name"
|
||||
/>
|
||||
<button class="btn btn-primary" on:click={fetchWikiImage}>Submit</button>
|
||||
</form>
|
||||
|
||||
{#if error}
|
||||
<p class="text-red-500">{error}</p>
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-primary" on:click={close}>Close</button>
|
||||
</div>
|
||||
</dialog>
|
|
@ -12,7 +12,7 @@
|
|||
let immichNextURL: string = '';
|
||||
let loading = false;
|
||||
|
||||
export let adventure: Location | null = null;
|
||||
export let location: Location | null = null;
|
||||
export let copyImmichLocally: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
@ -21,7 +21,7 @@
|
|||
let currentAlbum: string = '';
|
||||
|
||||
let selectedDate: string =
|
||||
(adventure as Location | null)?.visits
|
||||
(location as Location | null)?.visits
|
||||
.map((v) => new Date(v.end_date || v.start_date))
|
||||
.sort((a, b) => +b - +a)[0]
|
||||
?.toISOString()
|
||||
|
@ -57,8 +57,8 @@
|
|||
}
|
||||
|
||||
async function saveImmichRemoteUrl(imageId: string) {
|
||||
if (!adventure) {
|
||||
console.error('No adventure provided to save the image URL');
|
||||
if (!location) {
|
||||
console.error('No location provided to save the image URL');
|
||||
return;
|
||||
}
|
||||
let res = await fetch('/api/images', {
|
||||
|
@ -68,7 +68,7 @@
|
|||
},
|
||||
body: JSON.stringify({
|
||||
immich_id: imageId,
|
||||
adventure: adventure.id
|
||||
location: location.id
|
||||
})
|
||||
});
|
||||
if (res.ok) {
|
||||
|
|
|
@ -273,7 +273,7 @@
|
|||
{#if type != 'link'}
|
||||
<div class="flex justify-between items-center">
|
||||
<button
|
||||
class="btn btn-neutral btn-sm flex-1 mr-2"
|
||||
class="btn btn-base-300 btn-sm flex-1 mr-2"
|
||||
on:click={() => goto(`/locations/${adventure.id}`)}
|
||||
>
|
||||
<Launch class="w-4 h-4" />
|
||||
|
|
|
@ -847,7 +847,7 @@
|
|||
|
||||
{#if immichIntegration}
|
||||
<ImmichSelect
|
||||
adventure={location}
|
||||
{location}
|
||||
on:fetchImage={(e) => {
|
||||
url = e.detail;
|
||||
fetchImage();
|
||||
|
|
|
@ -17,6 +17,18 @@
|
|||
export let is_enabled: boolean;
|
||||
let reauthError: boolean = false;
|
||||
|
||||
// import Account from '~icons/mdi/account';
|
||||
import Clear from '~icons/mdi/close';
|
||||
import Check from '~icons/mdi/check-circle';
|
||||
import Copy from '~icons/mdi/content-copy';
|
||||
import Error from '~icons/mdi/alert-circle';
|
||||
import Key from '~icons/mdi/key';
|
||||
import QrCode from '~icons/mdi/qrcode';
|
||||
import Security from '~icons/mdi/security';
|
||||
import Warning from '~icons/mdi/alert';
|
||||
import Shield from '~icons/mdi/shield-account';
|
||||
import Backup from '~icons/mdi/backup-restore';
|
||||
|
||||
onMount(() => {
|
||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||
if (modal) {
|
||||
|
@ -113,72 +125,214 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<dialog id="my_modal_1" class="modal">
|
||||
<dialog id="my_modal_1" class="modal backdrop-blur-sm">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-lg">{$t('settings.enable_mfa')}</h3>
|
||||
<div
|
||||
class="modal-box w-11/12 max-w-4xl bg-gradient-to-br from-base-100 via-base-100 to-base-200 border border-base-300 shadow-2xl"
|
||||
role="dialog"
|
||||
on:keydown={handleKeydown}
|
||||
tabindex="0"
|
||||
>
|
||||
<!-- Header Section -->
|
||||
<div
|
||||
class=" top-0 z-10 bg-base-100/90 backdrop-blur-lg border-b border-base-300 -mx-6 -mt-6 px-6 py-4 mb-6"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-warning/10 rounded-xl">
|
||||
<Shield class="w-8 h-8 text-warning" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-warning bg-clip-text">
|
||||
{$t('settings.enable_mfa')}
|
||||
</h1>
|
||||
<p class="text-sm text-base-content/60">
|
||||
{$t('settings.secure_your_account')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if qrCodeDataUrl}
|
||||
<div class="mb-4 flex items-center justify-center mt-2">
|
||||
<img src={qrCodeDataUrl} alt="QR Code" class="w-64 h-64" />
|
||||
<!-- Status Badge -->
|
||||
<div class="hidden md:flex items-center gap-2">
|
||||
<div class="badge badge-warning badge-lg gap-2">
|
||||
<Security class="w-4 h-4" />
|
||||
{is_enabled ? $t('settings.enabled') : $t('settings.setup_required')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Close Button -->
|
||||
<button class="btn btn-ghost btn-square" on:click={close}>
|
||||
<Clear class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center justify-center mb-6">
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="px-2">
|
||||
<!-- QR Code Section -->
|
||||
{#if qrCodeDataUrl}
|
||||
<div class="card bg-base-200/50 border border-base-300/50 mb-6">
|
||||
<div class="card-body items-center text-center">
|
||||
<h3 class="card-title text-xl mb-4 flex items-center gap-2">
|
||||
<QrCode class="w-6 h-6 text-primary" />
|
||||
{$t('settings.scan_qr_code')}
|
||||
</h3>
|
||||
<div class="p-4 bg-white rounded-xl border border-base-300 mb-4">
|
||||
<img src={qrCodeDataUrl} alt="QR Code" class="w-64 h-64" />
|
||||
</div>
|
||||
<p class="text-base-content/60 max-w-md">
|
||||
{$t('settings.scan_with_authenticator_app')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Secret Key Section -->
|
||||
{#if secret}
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={secret}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
readonly
|
||||
/>
|
||||
<button class="btn btn-primary ml-2" on:click={() => copyToClipboard(secret)}
|
||||
>{$t('settings.copy')}</button
|
||||
>
|
||||
<div class="card bg-base-200/50 border border-base-300/50 mb-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg mb-4 flex items-center gap-2">
|
||||
<Key class="w-5 h-5 text-secondary" />
|
||||
{$t('settings.manual_entry')}
|
||||
</h3>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={secret}
|
||||
class="input input-bordered w-full font-mono text-sm bg-base-100/80"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<button class="btn btn-secondary gap-2" on:click={() => copyToClipboard(secret)}>
|
||||
<Copy class="w-4 h-4" />
|
||||
{$t('settings.copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Verification Code Section -->
|
||||
<div class="card bg-base-200/50 border border-base-300/50 mb-6">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg mb-4 flex items-center gap-2">
|
||||
<Shield class="w-5 h-5 text-success" />
|
||||
{$t('settings.verify_setup')}
|
||||
</h3>
|
||||
<div class="form-control">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">
|
||||
{$t('settings.authenticator_code')}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={$t('settings.enter_6_digit_code')}
|
||||
class="input input-bordered bg-base-100/80 font-mono text-center text-lg tracking-widest"
|
||||
bind:value={first_code}
|
||||
maxlength="6"
|
||||
/>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-base-content/60">
|
||||
{$t('settings.enter_code_from_app')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recovery Codes Section -->
|
||||
{#if recovery_codes.length > 0}
|
||||
<div class="card bg-base-200/50 border border-base-300/50 mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="card-title text-lg flex items-center gap-2">
|
||||
<Backup class="w-5 h-5 text-info" />
|
||||
{$t('settings.recovery_codes')}
|
||||
</h3>
|
||||
<button
|
||||
class="btn btn-info btn-sm gap-2"
|
||||
on:click={() => copyToClipboard(recovery_codes.join(', '))}
|
||||
>
|
||||
<Copy class="w-4 h-4" />
|
||||
{$t('settings.copy_all')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning mb-4">
|
||||
<Warning class="w-5 h-5" />
|
||||
<div>
|
||||
<h4 class="font-semibold">{$t('settings.important')}</h4>
|
||||
<p class="text-sm">{$t('settings.recovery_codes_desc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{#each recovery_codes as code, index}
|
||||
<div class="relative group">
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
class="input input-bordered input-sm w-full font-mono text-center bg-base-100/80 pr-10"
|
||||
readonly
|
||||
/>
|
||||
<button
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity btn btn-ghost btn-xs"
|
||||
on:click={() => copyToClipboard(code)}
|
||||
>
|
||||
<Copy class="w-3 h-3" />
|
||||
</button>
|
||||
<span
|
||||
class="absolute -top-2 -left-2 bg-base-content text-base-100 rounded-full w-5 h-5 text-xs flex items-center justify-center font-bold"
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if reauthError}
|
||||
<div class="alert alert-error mb-6">
|
||||
<Error class="w-5 h-5" />
|
||||
<div>
|
||||
<h4 class="font-semibold">{$t('settings.error_occurred')}</h4>
|
||||
<p class="text-sm">{$t('settings.reset_session_error')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder={$t('settings.authenticator_code')}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
bind:value={first_code}
|
||||
/>
|
||||
|
||||
<div class="recovery-codes-container">
|
||||
{#if recovery_codes.length > 0}
|
||||
<h3 class="mt-4 text-center font-bold text-lg">{$t('settings.recovery_codes')}</h3>
|
||||
<p class="text-center text-lg mb-2">
|
||||
{$t('settings.recovery_codes_desc')}
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-primary ml-2"
|
||||
on:click={() => copyToClipboard(recovery_codes.join(', '))}>{$t('settings.copy')}</button
|
||||
>
|
||||
{/if}
|
||||
<div class="recovery-codes-grid flex flex-wrap">
|
||||
{#each recovery_codes as code}
|
||||
<div
|
||||
class="recovery-code-item flex items-center justify-center m-2 w-full sm:w-1/2 md:w-1/3 lg:w-1/4"
|
||||
>
|
||||
<input type="text" value={code} class="input input-bordered w-full" readonly />
|
||||
</div>
|
||||
{/each}
|
||||
<!-- Footer Actions -->
|
||||
<div
|
||||
class="bottom-0 bg-base-100/90 backdrop-blur-lg border-t border-base-300 -mx-6 -mb-6 px-6 py-4 mt-6 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-base-content/60">
|
||||
{is_enabled
|
||||
? $t('settings.mfa_already_enabled')
|
||||
: $t('settings.complete_setup_to_enable')}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
{#if !is_enabled && first_code.length >= 6}
|
||||
<button class="btn btn-success gap-2" on:click={sendTotp}>
|
||||
<Shield class="w-4 h-4" />
|
||||
{$t('settings.enable_mfa')}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn btn-primary gap-2" on:click={close}>
|
||||
<Check class="w-4 h-4" />
|
||||
{$t('about.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if reauthError}
|
||||
<div class="alert alert-error mt-4">
|
||||
{$t('settings.reset_session_error')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !is_enabled}
|
||||
<button class="btn btn-primary mt-4" on:click={sendTotp}>{$t('settings.enable_mfa')}</button>
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-primary mt-4" on:click={close}>{$t('about.close')}</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export let appVersion = 'v0.10.0-main-06192025';
|
||||
export let appVersion = 'v0.10.0-main-06252025';
|
||||
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.10.0';
|
||||
export let appTitle = 'AdventureLog';
|
||||
export let copyrightYear = '2023-2025';
|
||||
|
|
|
@ -9,7 +9,15 @@
|
|||
"oss_attributions": "Open Source Quellenangaben",
|
||||
"other_attributions": "Weitere Hinweise finden Sie in der README-Datei.",
|
||||
"source_code": "Quellcode",
|
||||
"generic_attributions": "Melden Sie sich bei Adventurelog an, um Zuschreibungen für aktivierte Integrationen und Dienste anzuzeigen."
|
||||
"generic_attributions": "Melden Sie sich bei Adventurelog an, um Zuschreibungen für aktivierte Integrationen und Dienste anzuzeigen.",
|
||||
"attributions": "Zuschreibungen",
|
||||
"developer": "Entwickler",
|
||||
"license_info": "Lizenz",
|
||||
"sponsor": "Sponsor",
|
||||
"thank_you": "Vielen Dank, dass Sie Adventurelog verwendet haben!",
|
||||
"version": "Version",
|
||||
"view_changelog": "ChangeLog anzeigen",
|
||||
"view_license": "Lizenz anzeigen"
|
||||
},
|
||||
"adventures": {
|
||||
"activities": {},
|
||||
|
@ -250,7 +258,8 @@
|
|||
"visit_calendar": "Besuchen Sie den Kalender",
|
||||
"wiki_location_desc": "Zieht Auszug aus dem Wikipedia -Artikel, der dem Namen des Standorts entspricht.",
|
||||
"will_be_marked_location": "wird als besucht markiert, sobald der Standort gespeichert ist.",
|
||||
"no_locations_found": "Keine Standorte gefunden"
|
||||
"no_locations_found": "Keine Standorte gefunden",
|
||||
"image_modal_navigate": "Verwenden Sie Pfeiltasten oder klicken Sie, um zu navigieren"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Entdecken, planen und erkunden Sie mühelos",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"staff_status": "Personalstatus",
|
||||
"staff_user": "Personalbenutzer",
|
||||
"profile_info_desc": "Aktualisieren Sie Ihre persönlichen Daten und Ihr Profilbild",
|
||||
"invalid_credentials": "Ungültige Anmeldeinformationen"
|
||||
"invalid_credentials": "Ungültige Anmeldeinformationen",
|
||||
"backup_restore": "Backup",
|
||||
"backup_restore_desc": "Speichern Sie Ihre Daten oder stellen Sie sie in einer früheren Sicherungsdatei wieder her.",
|
||||
"complete_setup_to_enable": "Komplettes Setup, um MFA zu aktivieren",
|
||||
"copy_all": "Alle kopieren",
|
||||
"enter_6_digit_code": "Geben Sie einen 6 -stelligen Code ein",
|
||||
"enter_code_from_app": "Geben Sie den Code aus der App ein",
|
||||
"error_occurred": "Es ist ein Fehler aufgetreten",
|
||||
"important": "Wichtig",
|
||||
"manual_entry": "Manueller Eintritt",
|
||||
"mfa_already_enabled": "MFA bereits aktiviert",
|
||||
"mfa_required": "MFA erforderlich",
|
||||
"scan_qr_code": "Scannen QR -Code",
|
||||
"scan_with_authenticator_app": "Scannen Sie mit Authenticator App",
|
||||
"secure_your_account": "Sichern Sie Ihr Konto",
|
||||
"setup_required": "Setup erforderlich",
|
||||
"verify_setup": "Überprüfen Sie das Setup",
|
||||
"whats_included": "Was ist enthalten",
|
||||
"backup_your_data": "Sichern Sie Ihre Daten",
|
||||
"backup_your_data_desc": "Laden Sie eine vollständige Sicherung Ihrer Kontodaten, einschließlich Standorte, Sammlungen, Medien und Besuchen, herunter.",
|
||||
"data_override_acknowledge": "Ich bestätige, dass dies alle meine vorhandenen Daten überschreibt",
|
||||
"data_override_acknowledge_desc": "Diese Aktion ist irreversibel und ersetzt alle Standorte, Sammlungen und Besuche in Ihrem Konto.",
|
||||
"data_override_warning": "Daten überschreiben Warnung",
|
||||
"data_override_warning_desc": "Das Wiederherstellen von Daten ersetzt alle vorhandenen Daten (die in der Sicherung enthalten sind) in Ihrem Konto vollständig. \nDiese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"integrations_settings": "Integrationseinstellungen",
|
||||
"media": "Medien",
|
||||
"restore_data": "Daten wiederherstellen",
|
||||
"restore_data_desc": "Laden Sie eine Sicherungsdatei hoch, um Ihre Daten wiederherzustellen.",
|
||||
"select_backup_file": "Wählen Sie Sicherungsdatei",
|
||||
"world_travel_visits": "Weltreisebesuche"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_delete_error": "Fehler beim Löschen der Checkliste",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "Standort",
|
||||
"locations": "Standorte",
|
||||
"my_locations": "Meine Standorte"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "Backup herunterladen"
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -43,7 +43,15 @@
|
|||
"nominatim_2": "Sus datos están licenciados bajo la licencia ODbL.",
|
||||
"other_attributions": "Atribuciones adicionales se pueden encontrar en el archivo README.",
|
||||
"close": "Cerrar",
|
||||
"generic_attributions": "Inicie sesión en AdventureLog para ver las atribuciones para integraciones y servicios habilitados."
|
||||
"generic_attributions": "Inicie sesión en AdventureLog para ver las atribuciones para integraciones y servicios habilitados.",
|
||||
"attributions": "Atribuciones",
|
||||
"developer": "Revelador",
|
||||
"license_info": "Licencia",
|
||||
"sponsor": "Patrocinador",
|
||||
"thank_you": "¡Gracias por usar Adventurelog!",
|
||||
"version": "Versión",
|
||||
"view_changelog": "Ver ChangeLog",
|
||||
"view_license": "Ver licencia"
|
||||
},
|
||||
"home": {
|
||||
"hero_1": "Descubre las Aventuras Más Emocionantes del Mundo",
|
||||
|
@ -302,7 +310,8 @@
|
|||
"visit_calendar": "Visitar el calendario",
|
||||
"wiki_location_desc": "Extrae extracto del artículo de Wikipedia que coincide con el nombre de la ubicación.",
|
||||
"will_be_marked_location": "se marcará según lo visitado una vez que se guarde la ubicación.",
|
||||
"no_locations_found": "No se encontraron ubicaciones"
|
||||
"no_locations_found": "No se encontraron ubicaciones",
|
||||
"image_modal_navigate": "Use teclas de flecha o haga clic para navegar"
|
||||
},
|
||||
"worldtravel": {
|
||||
"all": "Todo",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"all_rights_reserved": "Reservados todos los derechos.",
|
||||
"email_verified_erorr_desc": "Su correo electrónico no pudo ser verificado. \nPor favor intente de nuevo.",
|
||||
"no_emai_set": "Sin conjunto de correo electrónico",
|
||||
"invalid_credentials": "Credenciales no válidas"
|
||||
"invalid_credentials": "Credenciales no válidas",
|
||||
"backup_restore": "Respaldo",
|
||||
"backup_restore_desc": "Guardar sus datos o restaurarlo desde un archivo de copia de seguridad anterior.",
|
||||
"complete_setup_to_enable": "Configuración completa para habilitar MFA",
|
||||
"copy_all": "Copiar todo",
|
||||
"enter_6_digit_code": "Ingrese el código de 6 dígitos",
|
||||
"enter_code_from_app": "Ingrese el código desde la aplicación",
|
||||
"error_occurred": "Se ha producido un error",
|
||||
"important": "Importante",
|
||||
"manual_entry": "Entrada manual",
|
||||
"mfa_already_enabled": "MFA ya habilitado",
|
||||
"mfa_required": "MFA requerido",
|
||||
"scan_qr_code": "Escanear el código QR",
|
||||
"scan_with_authenticator_app": "Escanear con la aplicación Authenticator",
|
||||
"secure_your_account": "Asegure su cuenta",
|
||||
"setup_required": "Configuración requerida",
|
||||
"verify_setup": "Verificar la configuración",
|
||||
"whats_included": "¿Qué está incluido?",
|
||||
"backup_your_data": "Haga una copia de seguridad de sus datos",
|
||||
"backup_your_data_desc": "Descargue una copia de seguridad completa de los datos de su cuenta, incluidas ubicaciones, colecciones, medios y visitas.",
|
||||
"data_override_acknowledge": "Reconozco que esto anulará todos mis datos existentes.",
|
||||
"data_override_acknowledge_desc": "Esta acción es irreversible y reemplazará todas las ubicaciones, colecciones y visitas en su cuenta.",
|
||||
"data_override_warning": "Advertencia de anulación de datos",
|
||||
"data_override_warning_desc": "La restauración de datos reemplazará completamente todos los datos existentes (que se incluyen en la copia de seguridad) en su cuenta. \nEsta acción no se puede deshacer.",
|
||||
"integrations_settings": "Configuración de integraciones",
|
||||
"media": "Medios de comunicación",
|
||||
"restore_data": "Restaurar datos",
|
||||
"restore_data_desc": "Cargue un archivo de copia de seguridad para restaurar sus datos.",
|
||||
"select_backup_file": "Seleccione el archivo de copia de seguridad",
|
||||
"world_travel_visits": "Visitas de viajes mundiales"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_delete_error": "Error al eliminar la lista de tareas",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "Ubicación",
|
||||
"locations": "Ubicación",
|
||||
"my_locations": "Mis ubicaciones"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "Descargar copia de seguridad"
|
||||
}
|
||||
|
|
|
@ -9,7 +9,15 @@
|
|||
"oss_attributions": "Attributions Open Source",
|
||||
"other_attributions": "Des attributions supplémentaires peuvent être trouvées dans le fichier README.",
|
||||
"source_code": "Code source",
|
||||
"generic_attributions": "Connectez-vous à AdventureLog pour afficher les attributions pour les intégrations et services activés."
|
||||
"generic_attributions": "Connectez-vous à AdventureLog pour afficher les attributions pour les intégrations et services activés.",
|
||||
"attributions": "Attributions",
|
||||
"developer": "Promoteur",
|
||||
"license_info": "Licence",
|
||||
"sponsor": "Parrainer",
|
||||
"thank_you": "Merci d'utiliser AdventureLog!",
|
||||
"version": "Version",
|
||||
"view_changelog": "Voir Changelog",
|
||||
"view_license": "Afficher la licence"
|
||||
},
|
||||
"adventures": {
|
||||
"activities": {},
|
||||
|
@ -250,7 +258,8 @@
|
|||
"visit_calendar": "Visiter le calendrier",
|
||||
"wiki_location_desc": "Tire un extrait de l'article de Wikipedia correspondant au nom de l'emplacement.",
|
||||
"will_be_marked_location": "sera marqué comme visité une fois l'emplacement enregistré.",
|
||||
"no_locations_found": "Aucun emplacement trouvé"
|
||||
"no_locations_found": "Aucun emplacement trouvé",
|
||||
"image_modal_navigate": "Utilisez des touches flèches ou cliquez pour naviguer"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"disconnected": "Déconnecté",
|
||||
"email_management": "Gestion des e-mails",
|
||||
"enter_last_name": "Entrez votre nom de famille",
|
||||
"invalid_credentials": "Des références non valides"
|
||||
"invalid_credentials": "Des références non valides",
|
||||
"backup_restore": "Sauvegarde",
|
||||
"backup_restore_desc": "Enregistrez vos données ou restaurez-les à partir d'un fichier de sauvegarde précédent.",
|
||||
"complete_setup_to_enable": "Configuration complète pour activer le MFA",
|
||||
"copy_all": "Copier tout",
|
||||
"enter_6_digit_code": "Entrez le code à 6 chiffres",
|
||||
"enter_code_from_app": "Entrez le code à partir de l'application",
|
||||
"error_occurred": "Une erreur s'est produite",
|
||||
"important": "Important",
|
||||
"manual_entry": "Entrée manuelle",
|
||||
"mfa_already_enabled": "MFA déjà activé",
|
||||
"mfa_required": "MFA requis",
|
||||
"scan_qr_code": "Scanner le code QR",
|
||||
"scan_with_authenticator_app": "Scanner avec l'application Authenticatrice",
|
||||
"secure_your_account": "Sécuriser votre compte",
|
||||
"setup_required": "Configuration requise",
|
||||
"verify_setup": "Vérifiez la configuration",
|
||||
"whats_included": "Ce qui est inclus",
|
||||
"backup_your_data": "Sauvegardez vos données",
|
||||
"backup_your_data_desc": "Téléchargez une sauvegarde complète des données de votre compte, y compris les emplacements, les collections, les médias et les visites.",
|
||||
"data_override_acknowledge": "Je reconnais que cela remplacera toutes mes données existantes",
|
||||
"data_override_acknowledge_desc": "Cette action est irréversible et remplacera tous les emplacements, collections et visites dans votre compte.",
|
||||
"data_override_warning": "Avertissement de remplacement des données",
|
||||
"data_override_warning_desc": "La restauration des données remplacera complètement toutes les données existantes (incluses dans la sauvegarde) dans votre compte. \nCette action ne peut pas être annulée.",
|
||||
"integrations_settings": "Paramètres des intégrations",
|
||||
"media": "Médias",
|
||||
"restore_data": "Restaurer les données",
|
||||
"restore_data_desc": "Téléchargez un fichier de sauvegarde pour restaurer vos données.",
|
||||
"select_backup_file": "Sélectionnez le fichier de sauvegarde",
|
||||
"world_travel_visits": "Visites de voyage dans le monde"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_delete_error": "Erreur lors de la suppression de la liste de contrôle",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "Emplacement",
|
||||
"locations": "Lieux",
|
||||
"my_locations": "Mes emplacements"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "Télécharger la sauvegarde"
|
||||
}
|
||||
|
|
|
@ -9,7 +9,15 @@
|
|||
"oss_attributions": "Attribuzioni Open Source",
|
||||
"other_attributions": "Ulteriori attribuzioni possono essere trovate nel file README.",
|
||||
"source_code": "Codice sorgente",
|
||||
"generic_attributions": "Accedi a AdventureLog per visualizzare le attribuzioni per integrazioni e servizi abilitati."
|
||||
"generic_attributions": "Accedi a AdventureLog per visualizzare le attribuzioni per integrazioni e servizi abilitati.",
|
||||
"attributions": "Attribuzioni",
|
||||
"developer": "Sviluppatore",
|
||||
"license_info": "Licenza",
|
||||
"sponsor": "Sponsor",
|
||||
"thank_you": "Grazie per aver usato AdventureLog!",
|
||||
"version": "Versione",
|
||||
"view_changelog": "Visualizza Changelog",
|
||||
"view_license": "Visualizza la licenza"
|
||||
},
|
||||
"adventures": {
|
||||
"activities": {},
|
||||
|
@ -250,7 +258,8 @@
|
|||
"visit_calendar": "Visita il calendario",
|
||||
"wiki_location_desc": "Estratto dall'articolo di Wikipedia che corrisponde al nome della posizione.",
|
||||
"will_be_marked_location": "sarà contrassegnato come visitato una volta salvata la posizione.",
|
||||
"no_locations_found": "Nessuna posizione trovata"
|
||||
"no_locations_found": "Nessuna posizione trovata",
|
||||
"image_modal_navigate": "Usa i tasti freccia o fai clic per navigare"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Scopri, pianifica ed esplora con facilità",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"staff_status": "Stato del personale",
|
||||
"staff_user": "Utente del personale",
|
||||
"password_auth": "Autenticazione della password",
|
||||
"invalid_credentials": "Credenziali non valide"
|
||||
"invalid_credentials": "Credenziali non valide",
|
||||
"backup_restore": "Backup",
|
||||
"backup_restore_desc": "Salva i tuoi dati o ripristinarli da un precedente file di backup.",
|
||||
"complete_setup_to_enable": "Setup completa per abilitare MFA",
|
||||
"copy_all": "Copia tutto",
|
||||
"enter_6_digit_code": "Immettere il codice a 6 cifre",
|
||||
"enter_code_from_app": "Immettere il codice dall'app",
|
||||
"error_occurred": "Si è verificato un errore",
|
||||
"important": "Importante",
|
||||
"manual_entry": "Ingresso manuale",
|
||||
"mfa_already_enabled": "MFA già abilitato",
|
||||
"mfa_required": "MFA richiesto",
|
||||
"scan_qr_code": "Scansionare il codice QR",
|
||||
"scan_with_authenticator_app": "Scansiona con l'app Authenticator",
|
||||
"secure_your_account": "Proteggere il tuo account",
|
||||
"setup_required": "Setup richiesto",
|
||||
"verify_setup": "Verifica la configurazione",
|
||||
"whats_included": "Cosa è incluso",
|
||||
"backup_your_data": "Eseguire il backup dei tuoi dati",
|
||||
"backup_your_data_desc": "Scarica un backup completo dei dati dell'account tra cui posizioni, raccolte, media e visite.",
|
||||
"data_override_acknowledge": "Riconosco che questo sovrascriverà tutti i miei dati esistenti",
|
||||
"data_override_acknowledge_desc": "Questa azione è irreversibile e sostituirà tutte le posizioni, le raccolte e le visite nel tuo account.",
|
||||
"data_override_warning": "Avviso di sovraccarico dei dati",
|
||||
"data_override_warning_desc": "Il ripristino dei dati sostituirà completamente tutti i dati esistenti (inclusi nel backup) nel tuo account. \nQuesta azione non può essere annullata.",
|
||||
"integrations_settings": "Impostazioni integrazioni",
|
||||
"media": "Media",
|
||||
"restore_data": "Ripristina i dati",
|
||||
"restore_data_desc": "Carica un file di backup per ripristinare i dati.",
|
||||
"select_backup_file": "Seleziona il file di backup",
|
||||
"world_travel_visits": "Visite di viaggio nel mondo"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_delete_error": "Errore durante l'eliminazione della lista di controllo",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "Posizione",
|
||||
"locations": "Luoghi",
|
||||
"my_locations": "Le mie posizioni"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "Scarica il backup"
|
||||
}
|
||||
|
|
|
@ -9,7 +9,15 @@
|
|||
"oss_attributions": "오픈 소스 속성",
|
||||
"other_attributions": "추가 속성은 README 파일에서 찾을 수 있습니다.",
|
||||
"source_code": "소스 코드",
|
||||
"generic_attributions": "Adventurelog에 로그인하여 활성화 된 통합 및 서비스에 대한 속성을보십시오."
|
||||
"generic_attributions": "Adventurelog에 로그인하여 활성화 된 통합 및 서비스에 대한 속성을보십시오.",
|
||||
"attributions": "속성",
|
||||
"developer": "개발자",
|
||||
"license_info": "특허",
|
||||
"sponsor": "스폰서",
|
||||
"thank_you": "Adventurelog를 사용해 주셔서 감사합니다!",
|
||||
"version": "버전",
|
||||
"view_changelog": "changelog를 봅니다",
|
||||
"view_license": "라이센스보기"
|
||||
},
|
||||
"adventures": {
|
||||
"actions": "행동",
|
||||
|
@ -250,7 +258,8 @@
|
|||
"visit_calendar": "캘린더를 방문하십시오",
|
||||
"wiki_location_desc": "위치 이름과 일치하는 Wikipedia 기사에서 발췌 한 내용을 가져옵니다.",
|
||||
"will_be_marked_location": "위치가 저장되면 방문한대로 표시됩니다.",
|
||||
"no_locations_found": "발견 된 위치는 없습니다"
|
||||
"no_locations_found": "발견 된 위치는 없습니다",
|
||||
"image_modal_navigate": "화살표 키를 사용하거나 클릭하여 탐색하십시오"
|
||||
},
|
||||
"auth": {
|
||||
"confirm_password": "비밀번호 확인",
|
||||
|
@ -589,7 +598,36 @@
|
|||
"social_auth_setup": "소셜 인증 설정",
|
||||
"staff_status": "직원 상태",
|
||||
"staff_user": "직원 사용자",
|
||||
"invalid_credentials": "잘못된 자격 증명"
|
||||
"invalid_credentials": "잘못된 자격 증명",
|
||||
"backup_restore": "지원",
|
||||
"backup_restore_desc": "데이터를 저장하거나 이전 백업 파일에서 복원하십시오.",
|
||||
"complete_setup_to_enable": "MFA를 활성화하기위한 완전한 설정",
|
||||
"copy_all": "모두 복사하십시오",
|
||||
"enter_6_digit_code": "6 자리 코드를 입력하십시오",
|
||||
"enter_code_from_app": "앱에서 코드를 입력하십시오",
|
||||
"error_occurred": "오류가 발생했습니다",
|
||||
"important": "중요한",
|
||||
"manual_entry": "수동 입력",
|
||||
"mfa_already_enabled": "MFA는 이미 활성화되었습니다",
|
||||
"mfa_required": "MFA가 필요합니다",
|
||||
"scan_qr_code": "QR 코드를 스캔하십시오",
|
||||
"scan_with_authenticator_app": "Authenticator 앱으로 스캔하십시오",
|
||||
"secure_your_account": "계정을 확보하십시오",
|
||||
"setup_required": "설정이 필요합니다",
|
||||
"verify_setup": "설정을 확인하십시오",
|
||||
"whats_included": "포함 된 내용",
|
||||
"backup_your_data": "데이터를 백업하십시오",
|
||||
"backup_your_data_desc": "위치, 컬렉션, 미디어 및 방문을 포함하여 계정 데이터의 전체 백업을 다운로드하십시오.",
|
||||
"data_override_acknowledge": "나는 이것이 나의 모든 기존 데이터를 무시할 것임을 인정합니다.",
|
||||
"data_override_acknowledge_desc": "이 조치는 돌이킬 수 없으며 계정의 모든 위치, 컬렉션 및 방문을 대체합니다.",
|
||||
"data_override_warning": "데이터를 우선적으로 경고합니다",
|
||||
"data_override_warning_desc": "데이터 복원은 계정의 모든 기존 데이터 (백업에 포함 된)를 완전히 대체합니다. \n이 조치는 취소 할 수 없습니다.",
|
||||
"integrations_settings": "통합 설정",
|
||||
"media": "메디아",
|
||||
"restore_data": "데이터를 복원하십시오",
|
||||
"restore_data_desc": "백업 파일을 업로드하여 데이터를 복원하십시오.",
|
||||
"select_backup_file": "백업 파일을 선택하십시오",
|
||||
"world_travel_visits": "세계 여행 방문"
|
||||
},
|
||||
"share": {
|
||||
"go_to_settings": "설정으로 이동",
|
||||
|
@ -721,5 +759,6 @@
|
|||
"location": "위치",
|
||||
"locations": "위치",
|
||||
"my_locations": "내 위치"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "백업을 다운로드하십시오"
|
||||
}
|
||||
|
|
|
@ -9,7 +9,15 @@
|
|||
"oss_attributions": "Open source gebruik",
|
||||
"other_attributions": "Aanvullende vermeldingen zijn te vinden in het README-bestand.",
|
||||
"source_code": "Broncode",
|
||||
"generic_attributions": "Log in op AdventUrelog om attributies te bekijken voor ingeschakelde integraties en services."
|
||||
"generic_attributions": "Log in op AdventUrelog om attributies te bekijken voor ingeschakelde integraties en services.",
|
||||
"attributions": "Attributies",
|
||||
"developer": "Ontwikkelaar",
|
||||
"license_info": "Licentie",
|
||||
"sponsor": "Sponsor",
|
||||
"thank_you": "Bedankt voor het gebruik van AdventUrelog!",
|
||||
"version": "Versie",
|
||||
"view_changelog": "Bekijk Changelog",
|
||||
"view_license": "Bekijk licentie"
|
||||
},
|
||||
"adventures": {
|
||||
"activities": {},
|
||||
|
@ -250,7 +258,8 @@
|
|||
"visit_calendar": "Bezoek de agenda",
|
||||
"wiki_location_desc": "Haalt fragment uit het Wikipedia -artikel dat overeenkomt met de naam van de locatie.",
|
||||
"will_be_marked_location": "wordt gemarkeerd als bezocht zodra de locatie is opgeslagen.",
|
||||
"no_locations_found": "Geen locaties gevonden"
|
||||
"no_locations_found": "Geen locaties gevonden",
|
||||
"image_modal_navigate": "Gebruik pijltoetsen of klik om te navigeren"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Ontdek, plan en verken met gemak",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"staff_status": "Status",
|
||||
"staff_user": "Personeelsgebruiker",
|
||||
"connected": "Aangesloten",
|
||||
"invalid_credentials": "Ongeldige referenties"
|
||||
"invalid_credentials": "Ongeldige referenties",
|
||||
"backup_restore": "Back -up",
|
||||
"backup_restore_desc": "Sla uw gegevens op of herstel deze in een eerder back -upbestand.",
|
||||
"complete_setup_to_enable": "Volledige installatie om MFA in te schakelen",
|
||||
"copy_all": "Kopieer alles",
|
||||
"enter_6_digit_code": "Voer 6 cijfercode in",
|
||||
"enter_code_from_app": "Voer de code uit van app",
|
||||
"error_occurred": "Er is een fout opgetreden",
|
||||
"important": "Belangrijk",
|
||||
"manual_entry": "Handmatig invoer",
|
||||
"mfa_already_enabled": "MFA al ingeschakeld",
|
||||
"setup_required": "Instellingen vereist",
|
||||
"verify_setup": "Controleer de installatie",
|
||||
"whats_included": "Wat is inbegrepen",
|
||||
"mfa_required": "MFA vereist",
|
||||
"scan_qr_code": "Scan QR -code",
|
||||
"scan_with_authenticator_app": "Scan met authenticator -app",
|
||||
"secure_your_account": "Beveilig uw account",
|
||||
"backup_your_data": "Back -up van uw gegevens",
|
||||
"backup_your_data_desc": "Download een volledige back -up van uw accountgegevens, inclusief locaties, collecties, media en bezoeken.",
|
||||
"data_override_acknowledge": "Ik erken dat dit al mijn bestaande gegevens zal overschrijven",
|
||||
"data_override_acknowledge_desc": "Deze actie is onomkeerbaar en vervangt alle locaties, collecties en bezoeken in uw account.",
|
||||
"data_override_warning": "Gegevensoverschrijdende waarschuwing",
|
||||
"data_override_warning_desc": "Het herstellen van gegevens zal alle bestaande gegevens (die in de back -up zijn opgenomen) in uw account volledig vervangen. \nDeze actie kan niet ongedaan worden gemaakt.",
|
||||
"integrations_settings": "Instellingen integraties",
|
||||
"media": "Media",
|
||||
"restore_data": "Herstel gegevens",
|
||||
"restore_data_desc": "Upload een back -upbestand om uw gegevens te herstellen.",
|
||||
"select_backup_file": "Selecteer back -upbestand",
|
||||
"world_travel_visits": "Wereldreizen bezoeken"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_delete_error": "Fout bij het verwijderen van de checklist",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "Locatie",
|
||||
"locations": "Locaties",
|
||||
"my_locations": "Mijn locaties"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "Download back -up"
|
||||
}
|
||||
|
|
|
@ -43,7 +43,15 @@
|
|||
"nominatim_2": "Deres data er lisensiert under ODbL-lisensen.",
|
||||
"other_attributions": "Ytterligere attribusjoner finnes i README-filen.",
|
||||
"close": "Lukk",
|
||||
"generic_attributions": "Logg inn på Adventurelog for å se attribusjoner for aktiverte integrasjoner og tjenester."
|
||||
"generic_attributions": "Logg inn på Adventurelog for å se attribusjoner for aktiverte integrasjoner og tjenester.",
|
||||
"attributions": "Attribusjoner",
|
||||
"developer": "Utvikler",
|
||||
"license_info": "Tillatelse",
|
||||
"sponsor": "Sponsor",
|
||||
"thank_you": "Takk for at du bruker Adventurelog!",
|
||||
"version": "Versjon",
|
||||
"view_changelog": "Se Changelog",
|
||||
"view_license": "Vis lisens"
|
||||
},
|
||||
"home": {
|
||||
"hero_1": "Oppdag verdens mest spennende eventyr",
|
||||
|
@ -302,7 +310,8 @@
|
|||
"visit_calendar": "Besøk kalenderen",
|
||||
"wiki_location_desc": "Trekker utdrag fra Wikipedia -artikkelen som samsvarer med navnet på stedet.",
|
||||
"will_be_marked_location": "vil bli merket som besøkt når stedet er lagret.",
|
||||
"no_locations_found": "Ingen steder funnet"
|
||||
"no_locations_found": "Ingen steder funnet",
|
||||
"image_modal_navigate": "Bruk piltastene eller klikk for å navigere"
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Liste over land",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"social_auth_setup": "Sosial autentiseringsoppsett",
|
||||
"staff_status": "Personalstatus",
|
||||
"staff_user": "Personalbruker",
|
||||
"invalid_credentials": "Ugyldig legitimasjon"
|
||||
"invalid_credentials": "Ugyldig legitimasjon",
|
||||
"backup_restore": "Sikkerhetskopi",
|
||||
"backup_restore_desc": "Lagre dataene dine eller gjenopprett dem fra en tidligere sikkerhetskopifil.",
|
||||
"complete_setup_to_enable": "Komplett oppsett for å aktivere MFA",
|
||||
"copy_all": "Kopier alle",
|
||||
"enter_6_digit_code": "Skriv inn 6 -sifret kode",
|
||||
"enter_code_from_app": "Skriv inn kode fra appen",
|
||||
"error_occurred": "Det har oppstått en feil",
|
||||
"important": "Viktig",
|
||||
"manual_entry": "Manuell oppføring",
|
||||
"mfa_already_enabled": "MFA er allerede aktivert",
|
||||
"mfa_required": "MFA kreves",
|
||||
"scan_qr_code": "Skann QR -kode",
|
||||
"scan_with_authenticator_app": "Skann med Authenticator -appen",
|
||||
"secure_your_account": "Sikre kontoen din",
|
||||
"setup_required": "Oppsett kreves",
|
||||
"verify_setup": "Bekreft oppsett",
|
||||
"whats_included": "Hva som er inkludert",
|
||||
"backup_your_data": "Sikkerhetskopiere dataene dine",
|
||||
"backup_your_data_desc": "Last ned en komplett sikkerhetskopi av kontodataene dine, inkludert steder, samlinger, media og besøk.",
|
||||
"data_override_acknowledge": "Jeg erkjenner at dette vil overstyre alle mine eksisterende data",
|
||||
"data_override_acknowledge_desc": "Denne handlingen er irreversibel og vil erstatte alle steder, samlinger og besøk på kontoen din.",
|
||||
"data_override_warning": "Data overstyrer advarsel",
|
||||
"data_override_warning_desc": "Gjenoppretting av data vil erstatte alle eksisterende data fullstendig (som er inkludert i sikkerhetskopien) på kontoen din. \nDenne handlingen kan ikke angres.",
|
||||
"integrations_settings": "Integrasjoner Innstillinger",
|
||||
"media": "Media",
|
||||
"restore_data": "Gjenopprett data",
|
||||
"restore_data_desc": "Last opp en sikkerhetskopifil for å gjenopprette dataene dine.",
|
||||
"select_backup_file": "Velg sikkerhetskopifil",
|
||||
"world_travel_visits": "Verdens reisebesøk"
|
||||
},
|
||||
"collection": {
|
||||
"collection_created": "Samling opprettet!",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "Sted",
|
||||
"locations": "Lokasjoner",
|
||||
"my_locations": "Mine lokasjoner"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "Last ned sikkerhetskopi"
|
||||
}
|
||||
|
|
|
@ -43,7 +43,15 @@
|
|||
"nominatim_2": "Ich dane są licencjonowane na licencji ODbL.",
|
||||
"other_attributions": "Dodatkowe atrybucje można znaleźć w pliku README.",
|
||||
"close": "Zamknij",
|
||||
"generic_attributions": "Zaloguj się do Adventurelog, aby wyświetlić atrybucje dla włączonych integracji i usług."
|
||||
"generic_attributions": "Zaloguj się do Adventurelog, aby wyświetlić atrybucje dla włączonych integracji i usług.",
|
||||
"attributions": "Przypisania",
|
||||
"developer": "Wywoływacz",
|
||||
"license_info": "Licencja",
|
||||
"sponsor": "Sponsor",
|
||||
"thank_you": "Dziękujemy za korzystanie z Adventurelog!",
|
||||
"version": "Wersja",
|
||||
"view_changelog": "Zobacz Changelog",
|
||||
"view_license": "Wyświetl licencję"
|
||||
},
|
||||
"home": {
|
||||
"hero_1": "Odkryj najbardziej ekscytujące podróże na świecie",
|
||||
|
@ -302,7 +310,8 @@
|
|||
"visit_calendar": "Odwiedź kalendarz",
|
||||
"wiki_location_desc": "Wyciąga fragment artykułu Wikipedii pasujący do nazwy lokalizacji.",
|
||||
"will_be_marked_location": "zostanie oznaczone jako odwiedzone po zapisaniu lokalizacji.",
|
||||
"no_locations_found": "Nie znaleziono żadnych lokalizacji"
|
||||
"no_locations_found": "Nie znaleziono żadnych lokalizacji",
|
||||
"image_modal_navigate": "Użyj klawiszy strzałek lub kliknij, aby nawigować"
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Lista krajów",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"social_auth_setup": "Konfiguracja uwierzytelniania społecznego",
|
||||
"staff_status": "Status personelu",
|
||||
"staff_user": "Użytkownik personelu",
|
||||
"invalid_credentials": "Nieprawidłowe poświadczenia"
|
||||
"invalid_credentials": "Nieprawidłowe poświadczenia",
|
||||
"backup_restore": "Kopia zapasowa",
|
||||
"backup_restore_desc": "Zapisz dane lub przywróć je z poprzedniego pliku kopii zapasowej.",
|
||||
"complete_setup_to_enable": "Pełna konfiguracja, aby włączyć MFA",
|
||||
"copy_all": "Kopiuj wszystko",
|
||||
"enter_6_digit_code": "Wprowadź 6 -cyfrowy kod",
|
||||
"enter_code_from_app": "Wprowadź kod z aplikacji",
|
||||
"error_occurred": "Wystąpił błąd",
|
||||
"important": "Ważny",
|
||||
"manual_entry": "Wpis ręczny",
|
||||
"mfa_already_enabled": "MFA już włączona",
|
||||
"mfa_required": "Wymagane MSZ",
|
||||
"scan_qr_code": "Skanuj kod QR",
|
||||
"scan_with_authenticator_app": "Skanuj za pomocą aplikacji Authenticator",
|
||||
"secure_your_account": "Zabezpiecz swoje konto",
|
||||
"setup_required": "Wymagana konfiguracja",
|
||||
"verify_setup": "Sprawdź konfigurację",
|
||||
"whats_included": "Co jest uwzględnione",
|
||||
"backup_your_data": "Kopie wykonuj kopię zapasową danych",
|
||||
"backup_your_data_desc": "Pobierz pełną kopię zapasową danych konta, w tym lokalizacji, kolekcji, mediów i wizyt.",
|
||||
"data_override_acknowledge": "Przyjmuję do wiadomości, że zastąpi to wszystkie moje istniejące dane",
|
||||
"data_override_acknowledge_desc": "Ta akcja jest nieodwracalna i zastąpi wszystkie lokalizacje, kolekcje i wizyty na Twoim koncie.",
|
||||
"data_override_warning": "Zastąpienie danych ostrzeżenia",
|
||||
"data_override_warning_desc": "Przywracanie danych całkowicie zastąpi wszystkie istniejące dane (które są zawarte w tworzeniu kopii zapasowej) na Twoim koncie. \nTego działania nie można cofnąć.",
|
||||
"integrations_settings": "Ustawienia integracji",
|
||||
"media": "Głoska bezdźwięczna",
|
||||
"restore_data": "Przywróć dane",
|
||||
"restore_data_desc": "Prześlij plik kopii zapasowej, aby przywrócić dane.",
|
||||
"select_backup_file": "Wybierz plik kopii zapasowej",
|
||||
"world_travel_visits": "Wizyty podróży na świecie"
|
||||
},
|
||||
"collection": {
|
||||
"collection_created": "Kolekcja została pomyślnie utworzona!",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "Lokalizacja",
|
||||
"locations": "Lokalizacje",
|
||||
"my_locations": "Moje lokalizacje"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "Pobierz kopię zapasową"
|
||||
}
|
||||
|
|
|
@ -43,7 +43,15 @@
|
|||
"nominatim_2": "Их данные лицензированы под лицензией ODbL.",
|
||||
"other_attributions": "Дополнительные атрибуции можно найти в файле README.",
|
||||
"generic_attributions": "Войдите в AdventureLog, чтобы просмотреть атрибуции для включённых интеграций и сервисов.",
|
||||
"close": "Закрыть"
|
||||
"close": "Закрыть",
|
||||
"attributions": "Атрибуты",
|
||||
"developer": "Разработчик",
|
||||
"license_info": "Лицензия",
|
||||
"sponsor": "Спонсор",
|
||||
"thank_you": "Спасибо за использование AdventureLog!",
|
||||
"version": "Версия",
|
||||
"view_changelog": "Посмотреть изменение изменений",
|
||||
"view_license": "Просмотреть лицензию"
|
||||
},
|
||||
"home": {
|
||||
"hero_1": "Откройте для себя самые захватывающие приключения мира",
|
||||
|
@ -302,7 +310,8 @@
|
|||
"visit_calendar": "Посетите календарь",
|
||||
"wiki_location_desc": "Вытягивает отрывок из статьи Википедии, соответствующей названию места.",
|
||||
"will_be_marked_location": "будет отмечен по посещению после сохранения местоположения.",
|
||||
"no_locations_found": "Никаких мест не найдено"
|
||||
"no_locations_found": "Никаких мест не найдено",
|
||||
"image_modal_navigate": "Используйте клавиши со стрелками или нажмите, чтобы перемещаться"
|
||||
},
|
||||
"worldtravel": {
|
||||
"country_list": "Список стран",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"enter_new_password": "Введите новый пароль",
|
||||
"connected": "Подключено",
|
||||
"disconnected": "Отключено",
|
||||
"invalid_credentials": "Неверные полномочия"
|
||||
"invalid_credentials": "Неверные полномочия",
|
||||
"backup_restore": "Резервная копия",
|
||||
"backup_restore_desc": "Сохраните данные или восстановите их из предыдущего файла резервного копирования.",
|
||||
"complete_setup_to_enable": "Полная установка, чтобы включить MFA",
|
||||
"copy_all": "Копировать все",
|
||||
"enter_6_digit_code": "Введите 6 -значный код",
|
||||
"enter_code_from_app": "Введите код из приложения",
|
||||
"error_occurred": "Произошла ошибка",
|
||||
"important": "Важный",
|
||||
"manual_entry": "Ручная запись",
|
||||
"mfa_already_enabled": "MFA уже включен",
|
||||
"mfa_required": "MFA требуется",
|
||||
"scan_qr_code": "Сканировать QR -код",
|
||||
"scan_with_authenticator_app": "Сканирование с помощью приложения аутентификатора",
|
||||
"secure_your_account": "Защитите свою учетную запись",
|
||||
"setup_required": "Настройка требуется",
|
||||
"verify_setup": "Проверьте настройку",
|
||||
"whats_included": "Что включено",
|
||||
"backup_your_data": "Резервную копию ваших данных",
|
||||
"backup_your_data_desc": "Загрузите полное резервное копирование данных вашей учетной записи, включая местоположения, коллекции, медиа и посещения.",
|
||||
"data_override_acknowledge": "Я признаю, что это переопределит все мои существующие данные",
|
||||
"data_override_acknowledge_desc": "Это действие необратимо и заменит все местоположения, коллекции и посещения в вашем аккаунте.",
|
||||
"data_override_warning": "Предупреждение о переопределении данных",
|
||||
"data_override_warning_desc": "Восстановление данных полностью заменит все существующие данные (которые включены в резервную копию) в вашу учетную запись. \nЭто действие не может быть отменено.",
|
||||
"integrations_settings": "Настройки интеграции",
|
||||
"media": "СМИ",
|
||||
"restore_data": "Восстановить данные",
|
||||
"restore_data_desc": "Загрузите файл резервного копирования, чтобы восстановить ваши данные.",
|
||||
"select_backup_file": "Выберите файл резервного копирования",
|
||||
"world_travel_visits": "Всемирные поездки"
|
||||
},
|
||||
"collection": {
|
||||
"collection_created": "Коллекция успешно создана!",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "Расположение",
|
||||
"locations": "Локации",
|
||||
"my_locations": "Мои локации"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "Скачать резервную копию"
|
||||
}
|
||||
|
|
|
@ -9,7 +9,15 @@
|
|||
"oss_attributions": "Tillskrivningar med öppen källkod",
|
||||
"other_attributions": "Ytterligare attributioner finns i README-filen.",
|
||||
"source_code": "Källkod",
|
||||
"generic_attributions": "Logga in på AdventureLog för att visa attribut för aktiverade integrationer och tjänster."
|
||||
"generic_attributions": "Logga in på AdventureLog för att visa attribut för aktiverade integrationer och tjänster.",
|
||||
"attributions": "Tillskrivningar",
|
||||
"developer": "Framkallare",
|
||||
"license_info": "Licens",
|
||||
"sponsor": "Sponsor",
|
||||
"thank_you": "Tack för att du använder AdventureLog!",
|
||||
"version": "Version",
|
||||
"view_changelog": "Visa Changelog",
|
||||
"view_license": "Visa licens"
|
||||
},
|
||||
"adventures": {
|
||||
"activities": {},
|
||||
|
@ -250,7 +258,8 @@
|
|||
"visit_calendar": "Besök kalendern",
|
||||
"wiki_location_desc": "Drar utdrag från Wikipedia -artikeln som matchar namnet på platsen.",
|
||||
"will_be_marked_location": "kommer att markeras som besöks när platsen har sparats.",
|
||||
"no_locations_found": "Inga platser hittades"
|
||||
"no_locations_found": "Inga platser hittades",
|
||||
"image_modal_navigate": "Använd pilnycklar eller klicka för att navigera"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "Upptäck, planera och utforska med lätthet",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"social_auth_setup": "Social autentiseringsinställning",
|
||||
"staff_status": "Personalstatus",
|
||||
"staff_user": "Personalanvändare",
|
||||
"invalid_credentials": "Ogiltiga referenser"
|
||||
"invalid_credentials": "Ogiltiga referenser",
|
||||
"backup_restore": "Säkerhetskopiering",
|
||||
"backup_restore_desc": "Spara dina data eller återställa dem från en tidigare säkerhetskopieringsfil.",
|
||||
"complete_setup_to_enable": "Komplett installation för att aktivera MFA",
|
||||
"copy_all": "Kopiera alla",
|
||||
"enter_6_digit_code": "Ange 6 siffror",
|
||||
"enter_code_from_app": "Ange kod från appen",
|
||||
"error_occurred": "Ett fel har inträffat",
|
||||
"important": "Viktig",
|
||||
"manual_entry": "Manuell inträde",
|
||||
"mfa_already_enabled": "MFA redan aktiverat",
|
||||
"mfa_required": "MFA krävs",
|
||||
"scan_qr_code": "Skanna QR -kod",
|
||||
"scan_with_authenticator_app": "Skanna med autentisatorapp",
|
||||
"secure_your_account": "Säkra ditt konto",
|
||||
"setup_required": "Installation krävs",
|
||||
"verify_setup": "Verifiera installationen",
|
||||
"whats_included": "Vad ingår",
|
||||
"backup_your_data": "Säkerhetskopiera dina data",
|
||||
"backup_your_data_desc": "Ladda ner en fullständig säkerhetskopia av dina kontodata inklusive platser, samlingar, media och besök.",
|
||||
"data_override_acknowledge": "Jag erkänner att detta kommer att åsidosätta alla mina befintliga uppgifter",
|
||||
"data_override_acknowledge_desc": "Denna åtgärd är irreversibel och kommer att ersätta alla platser, samlingar och besök på ditt konto.",
|
||||
"data_override_warning": "Data åsidosättande varning",
|
||||
"data_override_warning_desc": "Återställa data kommer helt att ersätta alla befintliga data (som ingår i säkerhetskopian) i ditt konto. \nDenna åtgärd kan inte ångras.",
|
||||
"integrations_settings": "Integrationsinställningar",
|
||||
"media": "Media",
|
||||
"restore_data": "Återställa data",
|
||||
"restore_data_desc": "Ladda upp en säkerhetskopieringsfil för att återställa dina data.",
|
||||
"select_backup_file": "Välj säkerhetskopieringsfil",
|
||||
"world_travel_visits": "Världsresebesök"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_delete_error": "Ett fel uppstod vid borttagning av checklista",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "Plats",
|
||||
"locations": "Plats",
|
||||
"my_locations": "Mina platser"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "Ladda ner säkerhetskopiering"
|
||||
}
|
||||
|
|
|
@ -43,7 +43,15 @@
|
|||
"other_attributions": "其他声明可以在 README 文件中找到。",
|
||||
"source_code": "源代码",
|
||||
"close": "关闭",
|
||||
"generic_attributions": "登录到AdventureLog以查看启用集成和服务的归因。"
|
||||
"generic_attributions": "登录到AdventureLog以查看启用集成和服务的归因。",
|
||||
"attributions": "归因",
|
||||
"developer": "开发人员",
|
||||
"license_info": "执照",
|
||||
"sponsor": "赞助",
|
||||
"thank_you": "感谢您使用冒险!",
|
||||
"version": "版本",
|
||||
"view_changelog": "查看ChangElog",
|
||||
"view_license": "查看许可证"
|
||||
},
|
||||
"home": {
|
||||
"desc_1": "轻松发现、规划和探索",
|
||||
|
@ -302,7 +310,8 @@
|
|||
"visit_calendar": "访问日历",
|
||||
"wiki_location_desc": "从Wikipedia文章中提取摘录,符合该位置的名称。",
|
||||
"will_be_marked_location": "保存位置后,将被标记为访问。",
|
||||
"no_locations_found": "找不到位置"
|
||||
"no_locations_found": "找不到位置",
|
||||
"image_modal_navigate": "使用箭头键或单击以导航"
|
||||
},
|
||||
"auth": {
|
||||
"forgot_password": "忘记密码?",
|
||||
|
@ -497,7 +506,36 @@
|
|||
"quick_actions": "快速动作",
|
||||
"region_updates": "区域更新",
|
||||
"region_updates_desc": "更新访问了地区和城市",
|
||||
"invalid_credentials": "无效的凭据"
|
||||
"invalid_credentials": "无效的凭据",
|
||||
"backup_restore": "备份",
|
||||
"backup_restore_desc": "保存数据或从以前的备份文件还原。",
|
||||
"complete_setup_to_enable": "完整的设置以启用MFA",
|
||||
"copy_all": "复制全部",
|
||||
"enter_6_digit_code": "输入6位数代码",
|
||||
"enter_code_from_app": "从应用程序输入代码",
|
||||
"error_occurred": "发生了错误",
|
||||
"important": "重要的",
|
||||
"manual_entry": "手动输入",
|
||||
"mfa_already_enabled": "MFA已经启用",
|
||||
"mfa_required": "需要MFA",
|
||||
"scan_qr_code": "扫描QR码",
|
||||
"scan_with_authenticator_app": "使用身份验证器应用程序扫描",
|
||||
"secure_your_account": "保护您的帐户",
|
||||
"setup_required": "需要设置",
|
||||
"verify_setup": "验证设置",
|
||||
"whats_included": "包括什么",
|
||||
"backup_your_data": "备份您的数据",
|
||||
"backup_your_data_desc": "下载您的帐户数据的完整备份,包括位置,集合,媒体和访问。",
|
||||
"data_override_acknowledge": "我承认这将覆盖我所有现有数据",
|
||||
"data_override_acknowledge_desc": "此操作是不可逆转的,将取代您帐户中的所有位置,收集和访问。",
|
||||
"data_override_warning": "数据覆盖警告",
|
||||
"data_override_warning_desc": "还原数据将完全替换帐户中的所有现有数据(备份中包含在备份中)。\n该动作不能撤消。",
|
||||
"integrations_settings": "集成设置",
|
||||
"media": "媒体",
|
||||
"restore_data": "还原数据",
|
||||
"restore_data_desc": "上传备份文件以还原数据。",
|
||||
"select_backup_file": "选择备份文件",
|
||||
"world_travel_visits": "世界旅行访问"
|
||||
},
|
||||
"checklist": {
|
||||
"checklist_delete_error": "删除清单时出错",
|
||||
|
@ -722,5 +760,6 @@
|
|||
"location": "地点",
|
||||
"locations": "位置",
|
||||
"my_locations": "我的位置"
|
||||
}
|
||||
},
|
||||
"settings_download_backup": "下载备份"
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ async function handleRequest(
|
|||
});
|
||||
}
|
||||
|
||||
const responseData = await response.text();
|
||||
const responseData = await response.arrayBuffer();
|
||||
// Create a new Headers object without the 'set-cookie' header
|
||||
const cleanHeaders = new Headers(response.headers);
|
||||
cleanHeaders.delete('set-cookie');
|
||||
|
|
|
@ -88,7 +88,9 @@
|
|||
|
||||
let notFound: boolean = false;
|
||||
let isEditModalOpen: boolean = false;
|
||||
let image_url: string | null = null;
|
||||
let adventure_images: { image: string; adventure: AdditionalLocation | null }[] = [];
|
||||
let modalInitialIndex: number = 0;
|
||||
let isImageModalOpen: boolean = false;
|
||||
|
||||
onMount(async () => {
|
||||
if (data.props.adventure) {
|
||||
|
@ -114,6 +116,19 @@
|
|||
geojson = null;
|
||||
await getGpxFiles();
|
||||
}
|
||||
|
||||
function closeImageModal() {
|
||||
isImageModalOpen = false;
|
||||
}
|
||||
|
||||
function openImageModal(imageIndex: number) {
|
||||
adventure_images = adventure.images.map(img => ({
|
||||
image: img.image,
|
||||
adventure: adventure
|
||||
}));
|
||||
modalInitialIndex = imageIndex;
|
||||
isImageModalOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if notFound}
|
||||
|
@ -139,8 +154,12 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
{#if image_url}
|
||||
<ImageDisplayModal image={image_url} on:close={() => (image_url = null)} {adventure} />
|
||||
{#if isImageModalOpen}
|
||||
<ImageDisplayModal
|
||||
images={adventure_images}
|
||||
initialIndex={modalInitialIndex}
|
||||
on:close={closeImageModal}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !adventure && !notFound}
|
||||
|
@ -176,7 +195,7 @@
|
|||
>
|
||||
<button
|
||||
class="w-full h-full p-0 bg-transparent border-0"
|
||||
on:click={() => (image_url = image.image)}
|
||||
on:click={() => openImageModal(i)}
|
||||
aria-label={`View full image of ${adventure.name}`}
|
||||
>
|
||||
<img src={image.image} class="w-full h-full object-cover" alt={adventure.name} />
|
||||
|
@ -728,13 +747,13 @@
|
|||
<div class="card-body">
|
||||
<h3 class="card-title text-lg mb-4">🖼️ {$t('adventures.images')}</h3>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{#each adventure.images as image}
|
||||
{#each adventure.images as image, index}
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="aspect-square bg-cover bg-center rounded-lg cursor-pointer transition-transform duration-200 group-hover:scale-105"
|
||||
style="background-image: url({image.image})"
|
||||
on:click={() => (image_url = image.image)}
|
||||
on:keydown={(e) => e.key === 'Enter' && (image_url = image.image)}
|
||||
on:click={() => openImageModal(index)}
|
||||
on:keydown={(e) => e.key === 'Enter' && openImageModal(index)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
></div>
|
||||
|
|
|
@ -262,7 +262,7 @@ export const actions: Actions = {
|
|||
return { success: true };
|
||||
}
|
||||
},
|
||||
changeEmail: async (event) => {
|
||||
restoreData: async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
|
@ -270,28 +270,51 @@ export const actions: Actions = {
|
|||
if (!sessionId) {
|
||||
return redirect(302, '/');
|
||||
}
|
||||
const formData = await event.request.formData();
|
||||
const new_email = formData.get('new_email') as string | null | undefined;
|
||||
if (!new_email) {
|
||||
return fail(400, { message: 'auth.email_required' });
|
||||
} else {
|
||||
|
||||
try {
|
||||
const formData = await event.request.formData();
|
||||
const file = formData.get('file') as File | null | undefined;
|
||||
const confirm = formData.get('confirm') as string | null | undefined;
|
||||
|
||||
if (!file || file.size === 0) {
|
||||
return fail(400, { message: 'settings.no_file_selected' });
|
||||
}
|
||||
|
||||
if (confirm !== 'yes') {
|
||||
return fail(400, { message: 'settings.confirmation_required' });
|
||||
}
|
||||
|
||||
let csrfToken = await fetchCSRFToken();
|
||||
let res = await fetch(`${endpoint}/auth/change-email/`, {
|
||||
|
||||
// Create FormData for the API request
|
||||
const apiFormData = new FormData();
|
||||
apiFormData.append('file', file);
|
||||
apiFormData.append('confirm', 'yes');
|
||||
|
||||
let res = await fetch(`${endpoint}/api/backup/import/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Referer: event.url.origin, // Include Referer header
|
||||
Referer: event.url.origin,
|
||||
Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
new_email
|
||||
})
|
||||
body: apiFormData
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return fail(res.status, await res.json());
|
||||
const errorData = await res.json();
|
||||
return fail(res.status, {
|
||||
message: errorData.code
|
||||
? `settings.restore_error_${errorData.code}`
|
||||
: 'settings.generic_error',
|
||||
details: errorData
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Restore error:', error);
|
||||
return fail(500, { message: 'settings.generic_error' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -20,12 +20,12 @@
|
|||
emails = data.props.emails;
|
||||
}
|
||||
|
||||
let new_password_disable_setting: boolean = false;
|
||||
let new_email: string = '';
|
||||
let public_url: string = data.props.publicUrl;
|
||||
let immichIntegration = data.props.immichIntegration;
|
||||
let googleMapsEnabled = data.props.googleMapsEnabled;
|
||||
let activeSection: string = 'profile';
|
||||
let acknowledgeRestoreOverride: boolean = false;
|
||||
|
||||
let newImmichIntegration: ImmichIntegration = {
|
||||
server_url: '',
|
||||
|
@ -41,6 +41,7 @@
|
|||
{ id: 'security', icon: '🔒', label: () => $t('settings.security') },
|
||||
{ id: 'emails', icon: '📧', label: () => $t('settings.emails') },
|
||||
{ id: 'integrations', icon: '🔗', label: () => $t('settings.integrations') },
|
||||
{ id: 'import_export', icon: '📦', label: () => $t('settings.backup_restore') },
|
||||
{ id: 'admin', icon: '⚙️', label: () => $t('settings.admin') },
|
||||
{ id: 'advanced', icon: '🛠️', label: () => $t('settings.advanced') }
|
||||
];
|
||||
|
@ -924,6 +925,198 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- import export -->
|
||||
{#if activeSection === 'import_export'}
|
||||
<div class="bg-base-100 rounded-2xl shadow-xl p-8">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="p-3 bg-accent/10 rounded-xl">
|
||||
<span class="text-2xl">📦</span>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold">{$t('settings.backup_restore')}</h2>
|
||||
<p class="text-base-content/70">
|
||||
{$t('settings.backup_restore_desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backup Coverage -->
|
||||
<div class="bg-base-200 rounded-xl p-4 mb-6">
|
||||
<h4 class="text-sm font-semibold mb-3 text-base-content/70">
|
||||
{$t('settings.whats_included')}
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<!-- Backed Up -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>📍 {$t('locations.locations')}</span>
|
||||
<span>✅</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>🚶 {$t('adventures.visits')}</span>
|
||||
<span>✅</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>📚 {$t('navbar.collections')}</span>
|
||||
<span>✅</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>🖼️ {$t('settings.media')}</span>
|
||||
<span>✅</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>🌍 {$t('settings.world_travel_visits')}</span>
|
||||
<span>✅</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Not Backed Up -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>⚙️ {$t('navbar.settings')}</span>
|
||||
<span>❌</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>👤 {$t('navbar.profile')}</span>
|
||||
<span>❌</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>🔗 {$t('settings.integrations_settings')}</span>
|
||||
<span>❌</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between opacity-30">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Backup Data -->
|
||||
<div class="p-6 bg-base-200 rounded-xl">
|
||||
<h3 class="text-lg font-semibold mb-4">📤 {$t('settings.backup_your_data')}</h3>
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{$t('settings.backup_your_data_desc')}
|
||||
</p>
|
||||
<div class="flex gap-4">
|
||||
<a class="btn btn-primary" href="/api/backup/export">
|
||||
💾 {$t('settings_download_backup')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Restore Data -->
|
||||
<div class="p-6 bg-base-200 rounded-xl">
|
||||
<h3 class="text-lg font-semibold mb-4">📥 {$t('settings.restore_data')}</h3>
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{$t('settings.restore_data_desc')}
|
||||
</p>
|
||||
|
||||
<!-- Warning Alert -->
|
||||
<div class="alert alert-warning mb-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<h4 class="font-bold">⚠️ {$t('settings.data_override_warning')}</h4>
|
||||
<p class="text-sm">
|
||||
{$t('settings.data_override_warning_desc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Form -->
|
||||
<form
|
||||
method="post"
|
||||
action="?/restoreData"
|
||||
use:enhance
|
||||
enctype="multipart/form-data"
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="form-control">
|
||||
<label class="label" for="backup-file">
|
||||
<span class="label-text font-medium"
|
||||
>{$t('settings.select_backup_file')}</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
id="backup-file"
|
||||
class="file-input file-input-bordered file-input-primary w-full"
|
||||
accept=".zip"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Acknowledgment Checkbox -->
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="confirm"
|
||||
value="yes"
|
||||
class="checkbox checkbox-warning"
|
||||
required
|
||||
bind:checked={acknowledgeRestoreOverride}
|
||||
/>
|
||||
<div>
|
||||
<span class="label-text font-medium text-warning"
|
||||
>{$t('settings.data_override_acknowledge')}</span
|
||||
>
|
||||
<p class="text-xs text-base-content/60 mt-1">
|
||||
{$t('settings.data_override_acknowledge_desc')}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if $page.form?.message && $page.form?.message.includes('restore')}
|
||||
<div class="alert alert-error">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current shrink-0 h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{$t($page.form?.message)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-warning"
|
||||
disabled={!acknowledgeRestoreOverride}
|
||||
>
|
||||
🚀 {$t('settings.restore_data')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Admin Section -->
|
||||
{#if activeSection === 'admin' && user.is_staff}
|
||||
<div class="bg-base-100 rounded-2xl shadow-xl p-8">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue