Undo, Redo 기능 추가
This commit is contained in:
@@ -266,6 +266,7 @@ function _moveSingle(srcIdx, targetIdx) {
|
||||
Array.from(_grid.children).forEach((c, i) => { c.dataset.idx = i; });
|
||||
_cb.updateNumberBadges();
|
||||
_cb.updateNewNameLabels();
|
||||
_cb.pushHistory?.([moved.path]);
|
||||
}
|
||||
|
||||
function _moveMulti(targetIdx) {
|
||||
@@ -301,6 +302,7 @@ function _moveMulti(targetIdx) {
|
||||
_cb.updateNewNameLabels();
|
||||
_cb.refreshCardSelection();
|
||||
_cb.updateSelCount();
|
||||
_cb.pushHistory?.(movedFiles.map(f => f.path));
|
||||
}
|
||||
|
||||
// ── 자동 스크롤 ───────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const MAX = 50;
|
||||
let _stack = []; // each entry: { files: [], affectedPaths: Set | null }
|
||||
let _cursor = -1;
|
||||
|
||||
export function push(files, affectedPaths = null) {
|
||||
_stack = _stack.slice(0, _cursor + 1);
|
||||
_stack.push({ files: [...files], affectedPaths: affectedPaths ? new Set(affectedPaths) : null });
|
||||
if (_stack.length > MAX) _stack.shift();
|
||||
_cursor = _stack.length - 1;
|
||||
}
|
||||
|
||||
// Returns { files, affectedPaths } where affectedPaths are from the entry being undone
|
||||
export function undo() {
|
||||
if (!canUndo()) return null;
|
||||
const affectedPaths = _stack[_cursor].affectedPaths;
|
||||
--_cursor;
|
||||
return { files: [..._stack[_cursor].files], affectedPaths };
|
||||
}
|
||||
|
||||
// Returns { files, affectedPaths } from the entry being redone
|
||||
export function redo() {
|
||||
if (!canRedo()) return null;
|
||||
++_cursor;
|
||||
return { files: [..._stack[_cursor].files], affectedPaths: _stack[_cursor].affectedPaths };
|
||||
}
|
||||
|
||||
export function canUndo() { return _cursor > 0; }
|
||||
export function canRedo() { return _cursor < _stack.length - 1; }
|
||||
|
||||
export function reset() {
|
||||
_stack = [];
|
||||
_cursor = -1;
|
||||
}
|
||||
@@ -32,6 +32,9 @@
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<button class="btn btn-info" id="btn-preview-toggle">미리보기</button>
|
||||
<div class="separator"></div>
|
||||
<button class="btn btn-ghost" id="btn-undo" disabled>↩ 되돌리기</button>
|
||||
<button class="btn btn-ghost" id="btn-redo" disabled>↪ 다시하기</button>
|
||||
</nav>
|
||||
|
||||
<form id="format-bar">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { state } from './state.js';
|
||||
import { showLoading, hideLoading, setStatus } from './ui.js';
|
||||
import { showLoading, hideLoading, setStatus, showToast } from './ui.js';
|
||||
import * as history from './history.js';
|
||||
import {
|
||||
renderGrid, updateSelCount, updateNumberBadges,
|
||||
updateNewNameLabels, refreshCardSelection, showEmptyGrid,
|
||||
@@ -28,10 +29,15 @@ const gridContainer = document.getElementById('grid-container');
|
||||
const dragGhostEl = document.getElementById('drag-ghost');
|
||||
const thumbSizeHint = document.getElementById('thumb-size-hint');
|
||||
const btnPreviewToggle = document.getElementById('btn-preview-toggle');
|
||||
const btnUndo = document.getElementById('btn-undo');
|
||||
const btnRedo = document.getElementById('btn-redo');
|
||||
const fileExplorerContainer = document.querySelector('.file-explorer');
|
||||
|
||||
// ── 핸들러 초기화 ─────────────────────────────────
|
||||
const callbacks = { refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels };
|
||||
const callbacks = {
|
||||
refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels,
|
||||
pushHistory: (affectedPaths) => { history.push(state.files, affectedPaths); updateHistoryButtons(); },
|
||||
};
|
||||
|
||||
pinchZoom.init({ gridContainer, thumbSizeHint });
|
||||
selection.init({ gridContainer, rubberBand, grid, state, callbacks });
|
||||
@@ -67,6 +73,8 @@ btnOpen.addEventListener('click', openFolder);
|
||||
btnSortName.addEventListener('click', () => sortFiles('name'));
|
||||
btnSortDate.addEventListener('click', () => sortFiles('date'));
|
||||
btnRename.addEventListener('click', doRename);
|
||||
btnUndo.addEventListener('click', performUndo);
|
||||
btnRedo.addEventListener('click', performRedo);
|
||||
btnPreviewToggle.addEventListener('click', () => {
|
||||
const on = btnPreviewToggle.classList.toggle('active');
|
||||
hoverPreview.setEnabled(on);
|
||||
@@ -82,6 +90,8 @@ document.addEventListener('keydown', (e) => {
|
||||
refreshCardSelection();
|
||||
updateSelCount();
|
||||
}
|
||||
if (e.code === 'KeyU') performUndo();
|
||||
if (e.code === 'KeyR') performRedo();
|
||||
});
|
||||
|
||||
// ── 폴더 열기 (다이얼로그) ───────────────────────
|
||||
@@ -131,6 +141,9 @@ async function loadFolder(folder) {
|
||||
renderGrid();
|
||||
updatePreview();
|
||||
btnRename.disabled = false;
|
||||
history.reset();
|
||||
history.push(state.files);
|
||||
updateHistoryButtons();
|
||||
}
|
||||
|
||||
// ── 정렬 ─────────────────────────────────────────
|
||||
@@ -143,6 +156,8 @@ async function sortFiles(mode) {
|
||||
updateSortButtons();
|
||||
await renderGrid();
|
||||
setStatus(`${mode === 'name' ? '이름순' : '날짜순'}으로 정렬됨`, true);
|
||||
history.push(state.files);
|
||||
updateHistoryButtons();
|
||||
}
|
||||
|
||||
function updateSortButtons() {
|
||||
@@ -150,6 +165,47 @@ function updateSortButtons() {
|
||||
btnSortDate.classList.toggle('active', state.sortMode === 'date');
|
||||
}
|
||||
|
||||
// ── Undo / Redo ───────────────────────────────────
|
||||
function updateHistoryButtons() {
|
||||
btnUndo.disabled = !history.canUndo();
|
||||
btnRedo.disabled = !history.canRedo();
|
||||
}
|
||||
|
||||
function performUndo() {
|
||||
if (!history.canUndo()) {
|
||||
showToast('더 이상 되돌릴 수 없습니다');
|
||||
return;
|
||||
}
|
||||
_applyHistorySnapshot(history.undo());
|
||||
}
|
||||
|
||||
function performRedo() {
|
||||
if (!history.canRedo()) {
|
||||
showToast('마지막 작업 상태입니다');
|
||||
return;
|
||||
}
|
||||
_applyHistorySnapshot(history.redo());
|
||||
}
|
||||
|
||||
function _applyHistorySnapshot({ files, affectedPaths }) {
|
||||
state.files.length = 0;
|
||||
state.files.push(...files);
|
||||
state.selectedIdxs.clear();
|
||||
|
||||
if (affectedPaths) {
|
||||
const pathToIdx = new Map(state.files.map((f, i) => [f.path, i]));
|
||||
affectedPaths.forEach(path => {
|
||||
const idx = pathToIdx.get(path);
|
||||
if (idx !== undefined) state.selectedIdxs.add(idx);
|
||||
});
|
||||
}
|
||||
|
||||
state.sortMode = null;
|
||||
updateSortButtons();
|
||||
renderGrid();
|
||||
updateHistoryButtons();
|
||||
}
|
||||
|
||||
// ── 포맷 미리보기 ─────────────────────────────────
|
||||
function updatePreview() {
|
||||
const prefix = inputPrefix.value.trim() || 'photo';
|
||||
|
||||
Reference in New Issue
Block a user