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; });
|
Array.from(_grid.children).forEach((c, i) => { c.dataset.idx = i; });
|
||||||
_cb.updateNumberBadges();
|
_cb.updateNumberBadges();
|
||||||
_cb.updateNewNameLabels();
|
_cb.updateNewNameLabels();
|
||||||
|
_cb.pushHistory?.([moved.path]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _moveMulti(targetIdx) {
|
function _moveMulti(targetIdx) {
|
||||||
@@ -301,6 +302,7 @@ function _moveMulti(targetIdx) {
|
|||||||
_cb.updateNewNameLabels();
|
_cb.updateNewNameLabels();
|
||||||
_cb.refreshCardSelection();
|
_cb.refreshCardSelection();
|
||||||
_cb.updateSelCount();
|
_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>
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
<button class="btn btn-info" id="btn-preview-toggle">미리보기</button>
|
<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>
|
</nav>
|
||||||
|
|
||||||
<form id="format-bar">
|
<form id="format-bar">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { state } from './state.js';
|
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 {
|
import {
|
||||||
renderGrid, updateSelCount, updateNumberBadges,
|
renderGrid, updateSelCount, updateNumberBadges,
|
||||||
updateNewNameLabels, refreshCardSelection, showEmptyGrid,
|
updateNewNameLabels, refreshCardSelection, showEmptyGrid,
|
||||||
@@ -28,10 +29,15 @@ const gridContainer = document.getElementById('grid-container');
|
|||||||
const dragGhostEl = document.getElementById('drag-ghost');
|
const dragGhostEl = document.getElementById('drag-ghost');
|
||||||
const thumbSizeHint = document.getElementById('thumb-size-hint');
|
const thumbSizeHint = document.getElementById('thumb-size-hint');
|
||||||
const btnPreviewToggle = document.getElementById('btn-preview-toggle');
|
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 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 });
|
pinchZoom.init({ gridContainer, thumbSizeHint });
|
||||||
selection.init({ gridContainer, rubberBand, grid, state, callbacks });
|
selection.init({ gridContainer, rubberBand, grid, state, callbacks });
|
||||||
@@ -67,6 +73,8 @@ btnOpen.addEventListener('click', openFolder);
|
|||||||
btnSortName.addEventListener('click', () => sortFiles('name'));
|
btnSortName.addEventListener('click', () => sortFiles('name'));
|
||||||
btnSortDate.addEventListener('click', () => sortFiles('date'));
|
btnSortDate.addEventListener('click', () => sortFiles('date'));
|
||||||
btnRename.addEventListener('click', doRename);
|
btnRename.addEventListener('click', doRename);
|
||||||
|
btnUndo.addEventListener('click', performUndo);
|
||||||
|
btnRedo.addEventListener('click', performRedo);
|
||||||
btnPreviewToggle.addEventListener('click', () => {
|
btnPreviewToggle.addEventListener('click', () => {
|
||||||
const on = btnPreviewToggle.classList.toggle('active');
|
const on = btnPreviewToggle.classList.toggle('active');
|
||||||
hoverPreview.setEnabled(on);
|
hoverPreview.setEnabled(on);
|
||||||
@@ -82,6 +90,8 @@ document.addEventListener('keydown', (e) => {
|
|||||||
refreshCardSelection();
|
refreshCardSelection();
|
||||||
updateSelCount();
|
updateSelCount();
|
||||||
}
|
}
|
||||||
|
if (e.code === 'KeyU') performUndo();
|
||||||
|
if (e.code === 'KeyR') performRedo();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 폴더 열기 (다이얼로그) ───────────────────────
|
// ── 폴더 열기 (다이얼로그) ───────────────────────
|
||||||
@@ -131,6 +141,9 @@ async function loadFolder(folder) {
|
|||||||
renderGrid();
|
renderGrid();
|
||||||
updatePreview();
|
updatePreview();
|
||||||
btnRename.disabled = false;
|
btnRename.disabled = false;
|
||||||
|
history.reset();
|
||||||
|
history.push(state.files);
|
||||||
|
updateHistoryButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 정렬 ─────────────────────────────────────────
|
// ── 정렬 ─────────────────────────────────────────
|
||||||
@@ -143,6 +156,8 @@ async function sortFiles(mode) {
|
|||||||
updateSortButtons();
|
updateSortButtons();
|
||||||
await renderGrid();
|
await renderGrid();
|
||||||
setStatus(`${mode === 'name' ? '이름순' : '날짜순'}으로 정렬됨`, true);
|
setStatus(`${mode === 'name' ? '이름순' : '날짜순'}으로 정렬됨`, true);
|
||||||
|
history.push(state.files);
|
||||||
|
updateHistoryButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSortButtons() {
|
function updateSortButtons() {
|
||||||
@@ -150,6 +165,47 @@ function updateSortButtons() {
|
|||||||
btnSortDate.classList.toggle('active', state.sortMode === 'date');
|
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() {
|
function updatePreview() {
|
||||||
const prefix = inputPrefix.value.trim() || 'photo';
|
const prefix = inputPrefix.value.trim() || 'photo';
|
||||||
|
|||||||
Reference in New Issue
Block a user