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:
parent
37b4ea179c
commit
19465077c0
20 changed files with 783 additions and 130 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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!",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": "설정으로 이동",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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!",
|
||||||
|
|
|
@ -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!",
|
||||||
|
|
|
@ -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": "Коллекция успешно создана!",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": "删除清单时出错",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue