1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-05 13:15:18 +02:00

Update backup export versioning and improve data restore warning message

This commit is contained in:
Sean Morley 2025-06-25 20:49:45 -04:00
parent 37b4ea179c
commit 19465077c0
20 changed files with 783 additions and 130 deletions

View file

@ -14,6 +14,7 @@ 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,
@ -38,9 +39,10 @@ class BackupViewSet(viewsets.ViewSet):
# Build export data structure
export_data = {
'version': '1.0',
'version': settings.ADVENTURELOG_RELEASE_VERSION,
'export_date': datetime.now().isoformat(),
'user_email': user.email,
'user_username': user.username,
'categories': [],
'collections': [],
'locations': [],
@ -83,7 +85,7 @@ class BackupViewSet(viewsets.ViewSet):
'end_date': collection.end_date.isoformat() if collection.end_date else None,
'is_archived': collection.is_archived,
'link': collection.link,
'shared_with_emails': list(collection.shared_with.values_list('email', flat=True))
'shared_with_user_ids': [str(uuid) for uuid in collection.shared_with.values_list('uuid', flat=True)]
})
# Create collection name to export_id mapping
@ -416,9 +418,10 @@ class BackupViewSet(viewsets.ViewSet):
summary['collections'] += 1
# Handle shared users
for email in col_data.get('shared_with_emails', []):
for uuid in col_data.get('shared_with_user_ids', []):
try:
shared_user = User.objects.get(email=email)
shared_user = User.objects.get(uuid=uuid)
if shared_user.public_profile:
collection.shared_with.add(shared_user)
except User.DoesNotExist:
pass

View file

@ -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'

View file

@ -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 ml-2 pointer-events-auto bg-white/80 border-none hover:bg-white text-gray-800 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-white/80 border-none hover:bg-white text-gray-800 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

View file

@ -5,14 +5,20 @@
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;
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 +31,10 @@
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
close();
} else if (event.key === 'ArrowLeft') {
previousSlide();
} else if (event.key === 'ArrowRight') {
nextSlide();
}
}
@ -33,26 +43,101 @@
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"
<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="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-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} 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"
@ -62,16 +147,122 @@
</svg>
</button>
</div>
<div
class="flex justify-center items-center"
style="display: flex; justify-content: center; align-items: center;"
</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={image}
alt={adventure.name}
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">Use arrow keys or click to 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>
Close
</button>
</div>
</div>
</div>
{/if}
</div>
</dialog>

View file

@ -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>
<!-- 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>
</div>
<!-- Main Content -->
<div class="px-2">
<!-- QR Code Section -->
{#if qrCodeDataUrl}
<div class="mb-4 flex items-center justify-center mt-2">
<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}
<div class="flex items-center justify-center mb-6">
<!-- Secret Key Section -->
{#if secret}
<div class="flex items-center">
<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"
placeholder={secret}
class="input input-bordered w-full max-w-xs"
value={secret}
class="input input-bordered w-full font-mono text-sm bg-base-100/80"
readonly
/>
<button class="btn btn-primary ml-2" on:click={() => copyToClipboard(secret)}
>{$t('settings.copy')}</button
>
</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}
</div>
<!-- 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.authenticator_code')}
class="input input-bordered w-full max-w-xs"
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>
<div class="recovery-codes-container">
<!-- Recovery Codes Section -->
{#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>
<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-primary ml-2"
on:click={() => copyToClipboard(recovery_codes.join(', '))}>{$t('settings.copy')}</button
class="btn btn-info btn-sm gap-2"
on:click={() => copyToClipboard(recovery_codes.join(', '))}
>
{/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"
<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)}
>
<input type="text" value={code} class="input input-bordered w-full" readonly />
<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>
{#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>
<!-- 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>
<button class="btn btn-primary mt-4" on:click={close}>{$t('about.close')}</button>
<!-- 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>
</div>
</dialog>

View file

@ -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';

View file

@ -497,7 +497,24 @@
"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"
},
"checklist": {
"checklist_delete_error": "Fehler beim Löschen der Checkliste",

View file

@ -497,7 +497,24 @@
"enter_new_password": "Enter new password",
"connected": "Connected",
"disconnected": "Disconnected",
"invalid_credentials": "Invalid credentials"
"invalid_credentials": "Invalid credentials",
"backup_restore": "Backup & Restore",
"backup_restore_desc": "Save your data or restore it from a previous backup file.",
"whats_included": "What's included",
"mfa_required": "MFA Required",
"secure_your_account": "Secure your account",
"setup_required": "Setup Required",
"scan_qr_code": "Scan QR Code",
"scan_with_authenticator_app": "Scan with authenticator app",
"manual_entry": "Manual Entry",
"verify_setup": "Verify Setup",
"enter_6_digit_code": "Enter 6 digit code",
"enter_code_from_app": "Enter code from app",
"copy_all": "Copy all",
"important": "Important",
"error_occurred": "An error has occurred",
"mfa_already_enabled": "MFA already enabled",
"complete_setup_to_enable": "Complete setup to enable MFA"
},
"collection": {
"collection_created": "Collection created successfully!",

View file

@ -497,7 +497,24 @@
"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?"
},
"checklist": {
"checklist_delete_error": "Error al eliminar la lista de tareas",

View file

@ -497,7 +497,24 @@
"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"
},
"checklist": {
"checklist_delete_error": "Erreur lors de la suppression de la liste de contrôle",

View file

@ -497,7 +497,24 @@
"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"
},
"checklist": {
"checklist_delete_error": "Errore durante l'eliminazione della lista di controllo",

View file

@ -589,7 +589,24 @@
"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": "포함 된 내용"
},
"share": {
"go_to_settings": "설정으로 이동",

View file

@ -497,7 +497,24 @@
"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"
},
"checklist": {
"checklist_delete_error": "Fout bij het verwijderen van de checklist",

View file

@ -497,7 +497,24 @@
"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"
},
"collection": {
"collection_created": "Samling opprettet!",

View file

@ -497,7 +497,24 @@
"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"
},
"collection": {
"collection_created": "Kolekcja została pomyślnie utworzona!",

View file

@ -497,7 +497,24 @@
"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": "Что включено"
},
"collection": {
"collection_created": "Коллекция успешно создана!",

View file

@ -497,7 +497,24 @@
"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"
},
"checklist": {
"checklist_delete_error": "Ett fel uppstod vid borttagning av checklista",

View file

@ -497,7 +497,24 @@
"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": "包括什么"
},
"checklist": {
"checklist_delete_error": "删除清单时出错",

View file

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

View file

@ -26,7 +26,6 @@
let googleMapsEnabled = data.props.googleMapsEnabled;
let activeSection: string = 'profile';
let acknowledgeRestoreOverride: boolean = false;
let fileInput: HTMLInputElement;
let newImmichIntegration: ImmichIntegration = {
server_url: '',
@ -935,9 +934,9 @@
</div>
<div>
<div>
<h2 class="text-2xl font-bold">Backup & Restore</h2>
<h2 class="text-2xl font-bold">{$t('settings.backup_restore')}</h2>
<p class="text-base-content/70">
Save your data or restore it from a previous backup file.
{$t('settings.backup_restore_desc')}
</p>
</div>
</div>
@ -945,7 +944,9 @@
<!-- 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">What's Included</h4>
<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">
@ -1030,8 +1031,8 @@
<div>
<h4 class="font-bold">⚠️ Data Override Warning</h4>
<p class="text-sm">
Restoring data will completely replace all existing data in your account.
This action cannot be undone.
Restoring data will completely replace all existing data (that is included
in the backup) in your account. This action cannot be undone.
</p>
</div>
</div>