mirror of
https://github.com/julyx10/lap.git
synced 2026-10-02 01:54:57 +08:00
feat(ui): remember main-window dialog positions across sessions
Add an optional positionKey prop to ModalDialog that persists x/y in localStorage under lap.dialog.pos.<key>. Position is restored on next mount (clamped to viewport) and saved once on unmount, so drag hot path stays zero-IO. First open with no saved data still centers as before. Wired for 11 main-window dialogs (manage-libraries, album-edit, import-organize, smart-album-edit, move-to, file-conflict, index-recovery, tag-picker, edit-tag, edit-collection, edit-comment). Cross-window components (MessageBox / TaggingDialog / AddToCollectionDialog) forward the prop; independent-window call sites in Settings and ImageViewer keep the original centered behavior.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog :title="$t('collection.edit_collections')" :width="500" @cancel="close">
|
||||
<ModalDialog :title="$t('collection.edit_collections')" :width="500" :position-key="positionKey" @cancel="close">
|
||||
<section class="space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="grow h-8 flex items-center rounded-box overflow-hidden bg-base-100 border border-neutral-content/30 focus-within:border-primary">
|
||||
@@ -131,7 +131,7 @@ import MessageBox from '@/components/MessageBox.vue';
|
||||
import ModalDialog from '@/components/ModalDialog.vue';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
|
||||
const props = defineProps<{ fileIds: number[] }>();
|
||||
const props = defineProps<{ fileIds: number[]; positionKey?: string }>();
|
||||
const emit = defineEmits(['applied', 'cancel', 'deleted']);
|
||||
const collections = ref<any[]>([]);
|
||||
const query = ref('');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog :title="isNewAlbum ? $t('album.edit.title_add') : $t('album.edit.title')" @cancel="clickCancel">
|
||||
<ModalDialog :title="isNewAlbum ? $t('album.edit.title_add') : $t('album.edit.title')" position-key="album-edit" @cancel="clickCancel">
|
||||
<section class="space-y-2 select-none">
|
||||
|
||||
<!-- General Information -->
|
||||
|
||||
@@ -608,6 +608,7 @@
|
||||
<TaggingDialog
|
||||
v-if="showTaggingDialog"
|
||||
:fileIds="fileIdsToTag"
|
||||
position-key="edit-tag"
|
||||
@ok="updateFileHasTags"
|
||||
@states-changed="syncTagStates"
|
||||
@cancel="showTaggingDialog = false"
|
||||
@@ -617,6 +618,7 @@
|
||||
<AddToCollectionDialog
|
||||
v-if="showAddToCollectionDialog"
|
||||
:fileIds="fileIdsToAddToCollection"
|
||||
position-key="edit-collection"
|
||||
@applied="handleCollectionsAdded"
|
||||
@deleted="handleCollectionDeleted"
|
||||
@cancel="showAddToCollectionDialog = false"
|
||||
@@ -632,6 +634,7 @@
|
||||
:multiLine="true"
|
||||
:OkText="$t('msgbox.ok')"
|
||||
:cancelText="$t('msgbox.cancel')"
|
||||
position-key="edit-comment"
|
||||
@ok="onEditComment"
|
||||
@cancel="showCommentMsgbox = false"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog :title="$t('msgbox.file_conflict.title')" :width="440" @cancel="resolve('skip')">
|
||||
<ModalDialog :title="$t('msgbox.file_conflict.title')" :width="440" position-key="file-conflict" @cancel="resolve('skip')">
|
||||
<p class="text-sm whitespace-pre-line wrap-break-word">
|
||||
{{ $t('msgbox.file_conflict.message', { name, destination }) }}
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog :title="$t('import_organize.import')" :width="520" @cancel="close">
|
||||
<ModalDialog :title="$t('import_organize.import')" :width="520" position-key="import-organize" @cancel="close">
|
||||
<div class="space-y-2 select-none">
|
||||
<section class="rounded-box p-2 space-y-2 bg-base-300/30 border border-base-content/5 shadow-sm">
|
||||
<div class="font-bold uppercase text-[10px] tracking-widest text-base-content/30">{{ $t('import_organize.section_folders') }}</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog :title="title" :width="440" @cancel="clickCancel">
|
||||
<ModalDialog :title="title" :width="440" position-key="index-recovery" @cancel="clickCancel">
|
||||
<div class="flex flex-col gap-2 select-none">
|
||||
<div v-if="message" class="text-sm wrap-break-word text-base-content/30">
|
||||
{{ message }}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
:title="$t('msgbox.manage_libraries.title')"
|
||||
:width="600"
|
||||
:height="400"
|
||||
position-key="manage-libraries"
|
||||
@cancel="clickCancel"
|
||||
>
|
||||
<div class="flex flex-col flex-1 min-h-0 border border-base-content/5 bg-base-300/30 shadow-sm rounded-box overflow-hidden relative">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog :title="title" :width="400" @cancel="clickCancel">
|
||||
<ModalDialog :title="title" :width="400" :position-key="positionKey" @cancel="clickCancel">
|
||||
<div v-if="message" class="text-sm whitespace-pre-line wrap-break-word select-none">
|
||||
{{ message }}
|
||||
</div>
|
||||
@@ -141,6 +141,12 @@ const props = defineProps({
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Optional key for persisting dialog position across sessions.
|
||||
// When empty, dialog always centers on mount (backward compatible).
|
||||
positionKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ const props = defineProps({
|
||||
height: {
|
||||
type: Number,
|
||||
default: undefined // undefined means no height limit
|
||||
},
|
||||
// When provided, dialog position is persisted in localStorage under this key
|
||||
// and restored on next mount. Without it, dialog always centers on mount.
|
||||
positionKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,10 +130,50 @@ const centerDialog = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const POS_PREFIX = 'lap.dialog.pos.';
|
||||
// Guards against persisting the initial (0,0) if the dialog unmounts
|
||||
// before the nextTick positioning pass has run.
|
||||
let positionReady = false;
|
||||
|
||||
const loadPosition = (): boolean => {
|
||||
if (!props.positionKey) return false;
|
||||
try {
|
||||
const raw = localStorage.getItem(POS_PREFIX + props.positionKey);
|
||||
if (!raw) return false;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (typeof parsed?.x !== 'number' || typeof parsed?.y !== 'number') return false;
|
||||
if (!Number.isFinite(parsed.x) || !Number.isFinite(parsed.y)) return false;
|
||||
x.value = parsed.x;
|
||||
y.value = parsed.y;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const savePosition = () => {
|
||||
if (!props.positionKey || !positionReady) return;
|
||||
try {
|
||||
localStorage.setItem(
|
||||
POS_PREFIX + props.positionKey,
|
||||
JSON.stringify({ x: Math.round(x.value), y: Math.round(y.value) })
|
||||
);
|
||||
} catch {
|
||||
// ignore quota / private-mode errors
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', clampPosition);
|
||||
nextTick(() => {
|
||||
centerDialog(); // Center on mount
|
||||
// Restore saved position if available, otherwise center.
|
||||
// Either way, clamp to viewport to handle resolution / display changes.
|
||||
if (!loadPosition()) {
|
||||
centerDialog();
|
||||
} else {
|
||||
clampPosition();
|
||||
}
|
||||
positionReady = true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,6 +184,9 @@ onUnmounted(() => {
|
||||
if (isDragging.value) {
|
||||
dragEnd();
|
||||
}
|
||||
|
||||
// Persist final position once per dialog session (on close), not per drag.
|
||||
savePosition();
|
||||
});
|
||||
|
||||
const clampPosition = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog :title="title" :width="500" @cancel="clickCancel">
|
||||
<ModalDialog :title="title" :width="500" position-key="move-to" @cancel="clickCancel">
|
||||
<!-- select album and folder -->
|
||||
<div class="h-[400px] overflow-auto">
|
||||
<AlbumList ref="albumListRef"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog :title="isNew ? $t('album.smart_edit.title_add') : $t('album.smart_edit.title_edit')" :width="620" @cancel="clickCancel">
|
||||
<ModalDialog :title="isNew ? $t('album.smart_edit.title_add') : $t('album.smart_edit.title_edit')" :width="620" position-key="smart-album-edit" @cancel="clickCancel">
|
||||
<section
|
||||
class="min-h-0 overflow-y-auto pr-1"
|
||||
@dragenter.stop
|
||||
|
||||
@@ -253,6 +253,7 @@
|
||||
: $t('msgbox.new_tag.title')
|
||||
"
|
||||
:width="400"
|
||||
position-key="tag-picker"
|
||||
@cancel="editor = null"
|
||||
>
|
||||
<form
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalDialog :title="$t('tag.edit_tag')" :width="500" @cancel="clickCancel">
|
||||
<ModalDialog :title="$t('tag.edit_tag')" :width="500" :position-key="positionKey" @cancel="clickCancel">
|
||||
<section class="space-y-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
@@ -273,6 +273,11 @@ const props = defineProps({
|
||||
type: Array as () => number[],
|
||||
default: () => [],
|
||||
},
|
||||
// Optional key for persisting dialog position across sessions.
|
||||
positionKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["ok", "cancel", "states-changed"]);
|
||||
|
||||
Reference in New Issue
Block a user