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.response import Response
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from django.conf import settings
from adventures.models import ( from adventures.models import (
Location, Collection, Transportation, Note, Checklist, ChecklistItem, Location, Collection, Transportation, Note, Checklist, ChecklistItem,
@ -38,9 +39,10 @@ class BackupViewSet(viewsets.ViewSet):
# Build export data structure # Build export data structure
export_data = { export_data = {
'version': '1.0', 'version': settings.ADVENTURELOG_RELEASE_VERSION,
'export_date': datetime.now().isoformat(), 'export_date': datetime.now().isoformat(),
'user_email': user.email, 'user_email': user.email,
'user_username': user.username,
'categories': [], 'categories': [],
'collections': [], 'collections': [],
'locations': [], 'locations': [],
@ -83,7 +85,7 @@ class BackupViewSet(viewsets.ViewSet):
'end_date': collection.end_date.isoformat() if collection.end_date else None, 'end_date': collection.end_date.isoformat() if collection.end_date else None,
'is_archived': collection.is_archived, 'is_archived': collection.is_archived,
'link': collection.link, '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 # Create collection name to export_id mapping
@ -416,10 +418,11 @@ class BackupViewSet(viewsets.ViewSet):
summary['collections'] += 1 summary['collections'] += 1
# Handle shared users # Handle shared users
for email in col_data.get('shared_with_emails', []): for uuid in col_data.get('shared_with_user_ids', []):
try: try:
shared_user = User.objects.get(email=email) shared_user = User.objects.get(uuid=uuid)
collection.shared_with.add(shared_user) if shared_user.public_profile:
collection.shared_with.add(shared_user)
except User.DoesNotExist: except User.DoesNotExist:
pass pass

View file

@ -326,6 +326,9 @@ LOGGING = {
# ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app') # 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 # https://github.com/dr5hn/countries-states-cities-database/tags
COUNTRY_REGION_JSON_VERSION = 'v2.6' COUNTRY_REGION_JSON_VERSION = 'v2.6'

View file

@ -6,7 +6,8 @@
export let adventures: Location[] = []; export let adventures: Location[] = [];
let currentSlide = 0; let currentSlide = 0;
let image_url: string | null = null; let showImageModal = false;
let modalInitialIndex = 0;
$: adventure_images = adventures.flatMap((adventure) => $: adventure_images = adventures.flatMap((adventure) =>
adventure.images.map((image) => ({ adventure.images.map((image) => ({
@ -42,13 +43,22 @@
currentSlide = currentSlide - 1; currentSlide = currentSlide - 1;
} }
} }
function openImageModal(initialIndex: number = currentSlide) {
modalInitialIndex = initialIndex;
showImageModal = true;
}
function closeImageModal() {
showImageModal = false;
}
</script> </script>
{#if image_url} {#if showImageModal && adventure_images.length > 0}
<ImageDisplayModal <ImageDisplayModal
adventure={adventure_images[currentSlide].adventure} images={adventure_images}
image={image_url} initialIndex={modalInitialIndex}
on:close={() => (image_url = null)} on:close={closeImageModal}
/> />
{/if} {/if}
@ -61,14 +71,46 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-missing-attribute --> <!-- svelte-ignore a11y-missing-attribute -->
<a <a
on:click|stopPropagation={() => (image_url = adventure_images[currentSlide].image)} on:click|stopPropagation={() => openImageModal(currentSlide)}
class="cursor-pointer" class="cursor-pointer relative group"
> >
<img <img
src={adventure_images[currentSlide].image} 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} 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> </a>
{#if adventure_images.length > 1} {#if adventure_images.length > 1}
@ -76,8 +118,18 @@
{#if currentSlide > 0} {#if currentSlide > 0}
<button <button
on:click|stopPropagation={() => changeSlide('prev')} 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} {:else}
<div class="w-12"></div> <div class="w-12"></div>
{/if} {/if}
@ -85,17 +137,43 @@
{#if currentSlide < adventure_images.length - 1} {#if currentSlide < adventure_images.length - 1}
<button <button
on:click|stopPropagation={() => changeSlide('next')} 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} {:else}
<div class="w-12"></div> <div class="w-12"></div>
{/if} {/if}
</div> </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} {/if}
</div> </div>
</div> </div>
{:else} {: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"> <div class="w-full h-48 bg-gradient-to-r from-success via-base to-primary relative">
<!-- subtle button bottom left text <!-- subtle button bottom left text
<div <div

View file

@ -5,14 +5,20 @@
let modal: HTMLDialogElement; let modal: HTMLDialogElement;
import type { Location } from '$lib/types'; import type { Location } from '$lib/types';
export let image: string; export let images: { image: string; adventure: Location | null }[] = [];
export let adventure: Location | null = null; export let initialIndex: number = 0;
let currentIndex = initialIndex;
let currentImage = images[currentIndex]?.image || '';
let currentAdventure = images[currentIndex]?.adventure || null;
onMount(() => { onMount(() => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement; modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) { if (modal) {
modal.showModal(); modal.showModal();
} }
// Set initial values
updateCurrentSlide(initialIndex);
}); });
function close() { function close() {
@ -25,6 +31,10 @@
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
close(); close();
} else if (event.key === 'ArrowLeft') {
previousSlide();
} else if (event.key === 'ArrowRight') {
nextSlide();
} }
} }
@ -33,44 +43,225 @@
close(); 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> </script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <dialog id="my_modal_1" class="modal backdrop-blur-sm" on:click={handleClickOutside}>
<dialog id="my_modal_1" class="modal" on:click={handleClickOutside}>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box w-11/12 max-w-5xl" role="dialog" on:keydown={handleKeydown} tabindex="0"> <div
{#if adventure} 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"
<div class="modal-header flex justify-between items-center mb-4"> role="dialog"
<h3 class="font-bold text-2xl">{adventure.name}</h3> on:keydown={handleKeydown}
<button class="btn btn-circle btn-neutral" on:click={close}> tabindex="0"
<svg >
xmlns="http://www.w3.org/2000/svg" {#if currentAdventure && currentImage}
class="h-6 w-6" <!-- Header -->
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 <div
class="flex justify-center 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"
style="display: flex; justify-content: center; align-items: center;"
> >
<img <div class="flex items-center justify-between">
src={image} <div class="flex items-center gap-3">
alt={adventure.name} <div class="p-2 bg-primary/10 rounded-xl">
style="max-width: 100%; max-height: 75vh; object-fit: contain;" <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"
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">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> </div>
{/if} {/if}
</div> </div>

View file

@ -17,6 +17,18 @@
export let is_enabled: boolean; export let is_enabled: boolean;
let reauthError: boolean = false; 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(() => { onMount(() => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement; modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) { if (modal) {
@ -113,72 +125,214 @@
} }
</script> </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-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0"> <div
<h3 class="font-bold text-lg">{$t('settings.enable_mfa')}</h3> 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} <!-- Status Badge -->
<div class="mb-4 flex items-center justify-center mt-2"> <div class="hidden md:flex items-center gap-2">
<img src={qrCodeDataUrl} alt="QR Code" class="w-64 h-64" /> <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>
{/if} </div>
<div class="flex items-center justify-center mb-6">
<!-- 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} {#if secret}
<div class="flex items-center"> <div class="card bg-base-200/50 border border-base-300/50 mb-6">
<input <div class="card-body">
type="text" <h3 class="card-title text-lg mb-4 flex items-center gap-2">
placeholder={secret} <Key class="w-5 h-5 text-secondary" />
class="input input-bordered w-full max-w-xs" {$t('settings.manual_entry')}
readonly </h3>
/> <div class="flex items-center gap-3">
<button class="btn btn-primary ml-2" on:click={() => copyToClipboard(secret)} <div class="flex-1">
>{$t('settings.copy')}</button <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> </div>
{/if} {/if}
</div> </div>
<input <!-- Footer Actions -->
type="text" <div
placeholder={$t('settings.authenticator_code')} 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"
class="input input-bordered w-full max-w-xs" >
bind:value={first_code} <div class="flex items-center justify-between">
/> <div class="text-sm text-base-content/60">
{is_enabled
<div class="recovery-codes-container"> ? $t('settings.mfa_already_enabled')
{#if recovery_codes.length > 0} : $t('settings.complete_setup_to_enable')}
<h3 class="mt-4 text-center font-bold text-lg">{$t('settings.recovery_codes')}</h3> </div>
<p class="text-center text-lg mb-2"> <div class="flex items-center gap-3">
{$t('settings.recovery_codes_desc')} {#if !is_enabled && first_code.length >= 6}
</p> <button class="btn btn-success gap-2" on:click={sendTotp}>
<button <Shield class="w-4 h-4" />
class="btn btn-primary ml-2" {$t('settings.enable_mfa')}
on:click={() => copyToClipboard(recovery_codes.join(', '))}>{$t('settings.copy')}</button </button>
> {/if}
{/if} <button class="btn btn-primary gap-2" on:click={close}>
<div class="recovery-codes-grid flex flex-wrap"> <Check class="w-4 h-4" />
{#each recovery_codes as code} {$t('about.close')}
<div </button>
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" </div>
>
<input type="text" value={code} class="input input-bordered w-full" readonly />
</div>
{/each}
</div> </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> </div>
</dialog> </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 versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.10.0';
export let appTitle = 'AdventureLog'; export let appTitle = 'AdventureLog';
export let copyrightYear = '2023-2025'; export let copyrightYear = '2023-2025';

View file

@ -497,7 +497,24 @@
"staff_status": "Personalstatus", "staff_status": "Personalstatus",
"staff_user": "Personalbenutzer", "staff_user": "Personalbenutzer",
"profile_info_desc": "Aktualisieren Sie Ihre persönlichen Daten und Ihr Profilbild", "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": {
"checklist_delete_error": "Fehler beim Löschen der Checkliste", "checklist_delete_error": "Fehler beim Löschen der Checkliste",

View file

@ -497,7 +497,24 @@
"enter_new_password": "Enter new password", "enter_new_password": "Enter new password",
"connected": "Connected", "connected": "Connected",
"disconnected": "Disconnected", "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": {
"collection_created": "Collection created successfully!", "collection_created": "Collection created successfully!",

View file

@ -497,7 +497,24 @@
"all_rights_reserved": "Reservados todos los derechos.", "all_rights_reserved": "Reservados todos los derechos.",
"email_verified_erorr_desc": "Su correo electrónico no pudo ser verificado. \nPor favor intente de nuevo.", "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", "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": {
"checklist_delete_error": "Error al eliminar la lista de tareas", "checklist_delete_error": "Error al eliminar la lista de tareas",

View file

@ -497,7 +497,24 @@
"disconnected": "Déconnecté", "disconnected": "Déconnecté",
"email_management": "Gestion des e-mails", "email_management": "Gestion des e-mails",
"enter_last_name": "Entrez votre nom de famille", "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": {
"checklist_delete_error": "Erreur lors de la suppression de la liste de contrôle", "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_status": "Stato del personale",
"staff_user": "Utente del personale", "staff_user": "Utente del personale",
"password_auth": "Autenticazione della password", "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": {
"checklist_delete_error": "Errore durante l'eliminazione della lista di controllo", "checklist_delete_error": "Errore durante l'eliminazione della lista di controllo",

View file

@ -589,7 +589,24 @@
"social_auth_setup": "소셜 인증 설정", "social_auth_setup": "소셜 인증 설정",
"staff_status": "직원 상태", "staff_status": "직원 상태",
"staff_user": "직원 사용자", "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": { "share": {
"go_to_settings": "설정으로 이동", "go_to_settings": "설정으로 이동",

View file

@ -497,7 +497,24 @@
"staff_status": "Status", "staff_status": "Status",
"staff_user": "Personeelsgebruiker", "staff_user": "Personeelsgebruiker",
"connected": "Aangesloten", "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": {
"checklist_delete_error": "Fout bij het verwijderen van de checklist", "checklist_delete_error": "Fout bij het verwijderen van de checklist",

View file

@ -497,7 +497,24 @@
"social_auth_setup": "Sosial autentiseringsoppsett", "social_auth_setup": "Sosial autentiseringsoppsett",
"staff_status": "Personalstatus", "staff_status": "Personalstatus",
"staff_user": "Personalbruker", "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": {
"collection_created": "Samling opprettet!", "collection_created": "Samling opprettet!",

View file

@ -497,7 +497,24 @@
"social_auth_setup": "Konfiguracja uwierzytelniania społecznego", "social_auth_setup": "Konfiguracja uwierzytelniania społecznego",
"staff_status": "Status personelu", "staff_status": "Status personelu",
"staff_user": "Użytkownik 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": {
"collection_created": "Kolekcja została pomyślnie utworzona!", "collection_created": "Kolekcja została pomyślnie utworzona!",

View file

@ -497,7 +497,24 @@
"enter_new_password": "Введите новый пароль", "enter_new_password": "Введите новый пароль",
"connected": "Подключено", "connected": "Подключено",
"disconnected": "Отключено", "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": {
"collection_created": "Коллекция успешно создана!", "collection_created": "Коллекция успешно создана!",

View file

@ -497,7 +497,24 @@
"social_auth_setup": "Social autentiseringsinställning", "social_auth_setup": "Social autentiseringsinställning",
"staff_status": "Personalstatus", "staff_status": "Personalstatus",
"staff_user": "Personalanvändare", "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": {
"checklist_delete_error": "Ett fel uppstod vid borttagning av checklista", "checklist_delete_error": "Ett fel uppstod vid borttagning av checklista",

View file

@ -497,7 +497,24 @@
"quick_actions": "快速动作", "quick_actions": "快速动作",
"region_updates": "区域更新", "region_updates": "区域更新",
"region_updates_desc": "更新访问了地区和城市", "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": {
"checklist_delete_error": "删除清单时出错", "checklist_delete_error": "删除清单时出错",

View file

@ -88,7 +88,9 @@
let notFound: boolean = false; let notFound: boolean = false;
let isEditModalOpen: 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 () => { onMount(async () => {
if (data.props.adventure) { if (data.props.adventure) {
@ -114,6 +116,19 @@
geojson = null; geojson = null;
await getGpxFiles(); 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> </script>
{#if notFound} {#if notFound}
@ -139,8 +154,12 @@
/> />
{/if} {/if}
{#if image_url} {#if isImageModalOpen}
<ImageDisplayModal image={image_url} on:close={() => (image_url = null)} {adventure} /> <ImageDisplayModal
images={adventure_images}
initialIndex={modalInitialIndex}
on:close={closeImageModal}
/>
{/if} {/if}
{#if !adventure && !notFound} {#if !adventure && !notFound}
@ -176,7 +195,7 @@
> >
<button <button
class="w-full h-full p-0 bg-transparent border-0" 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}`} aria-label={`View full image of ${adventure.name}`}
> >
<img src={image.image} class="w-full h-full object-cover" alt={adventure.name} /> <img src={image.image} class="w-full h-full object-cover" alt={adventure.name} />
@ -728,13 +747,13 @@
<div class="card-body"> <div class="card-body">
<h3 class="card-title text-lg mb-4">🖼️ {$t('adventures.images')}</h3> <h3 class="card-title text-lg mb-4">🖼️ {$t('adventures.images')}</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2"> <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="relative group">
<div <div
class="aspect-square bg-cover bg-center rounded-lg cursor-pointer transition-transform duration-200 group-hover:scale-105" 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})" style="background-image: url({image.image})"
on:click={() => (image_url = image.image)} on:click={() => openImageModal(index)}
on:keydown={(e) => e.key === 'Enter' && (image_url = image.image)} on:keydown={(e) => e.key === 'Enter' && openImageModal(index)}
role="button" role="button"
tabindex="0" tabindex="0"
></div> ></div>

View file

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