bueckerlars_obsidian-note-m.../src/modals/PreviewModal.ts

318 lines
10 KiB
TypeScript

import { Setting, App, TFile } from 'obsidian';
import { MovePreview, PreviewEntry } from '../types/MovePreview';
import NoteMoverShortcutPlugin from 'main';
import { NoticeManager } from '../utils/NoticeManager';
import { MobileUtils } from '../utils/MobileUtils';
import { combinePath, ensureFolderExists } from '../utils/PathUtils';
import { handleError, createError } from '../utils/Error';
import { BaseModal, BaseModalOptions } from './BaseModal';
export class PreviewModal extends BaseModal {
private movePreview: MovePreview;
private actionFooterEl: HTMLElement | null = null;
constructor(
app: App,
private plugin: NoteMoverShortcutPlugin,
movePreview: MovePreview,
options: BaseModalOptions = {}
) {
super(app, {
title: 'Move Preview',
titleIcon: '🔍',
cssClass: 'note-mover-preview-modal',
size: 'large',
...options,
});
this.movePreview = movePreview;
}
protected createContent(): void {
const { contentEl } = this;
// Subtitle with statistics
const stats = this.movePreview;
const subtitle = `${stats.totalFiles} files analyzed • ${stats.successfulMoves.length} will be moved`;
contentEl.createEl('p', {
text: subtitle,
cls: 'noteMover-modal-subtitle',
});
// Settings info removed - Rules and Filter are always enabled now
// Successful moves section
if (stats.successfulMoves.length > 0) {
this.createSuccessfulMovesSection(contentEl, stats.successfulMoves);
}
// Blocked moves section removed - only files that will be moved are shown
// No files message
if (stats.totalFiles === 0) {
contentEl.createEl('div', {
cls: 'noteMover-modal-empty',
text: 'No files found to analyze.',
});
}
// Action buttons
this.createActionButtons(contentEl);
}
private createSuccessfulMovesSection(
container: HTMLElement,
entries: PreviewEntry[]
) {
const isMobile = MobileUtils.isMobile();
const section = this.createSection(
container,
isMobile
? 'noteMover-preview-section noteMover-preview-section-success noteMover-preview-section-mobile'
: 'noteMover-preview-section noteMover-preview-section-success'
);
const header = section.createEl('div', {
cls: 'noteMover-modal-section-header',
});
header.innerHTML = `<h3>✅ Files to be moved (${entries.length})</h3>`;
const list = section.createEl('div', {
cls: isMobile
? 'noteMover-modal-list noteMover-modal-list-mobile'
: 'noteMover-modal-list',
});
entries.forEach(entry => {
const item = list.createEl('div', {
cls: isMobile
? 'noteMover-modal-list-item noteMover-preview-item-success noteMover-preview-item-mobile'
: 'noteMover-modal-list-item noteMover-preview-item-success',
});
const mainInfo = item.createEl('div', {
cls: 'noteMover-preview-item-main',
});
const fileName = mainInfo.createEl('div', {
cls: 'noteMover-preview-item-filename',
});
fileName.textContent = entry.fileName;
const pathInfo = mainInfo.createEl('div', {
cls: isMobile
? 'noteMover-preview-item-paths noteMover-preview-item-paths-mobile'
: 'noteMover-preview-item-paths',
});
if (isMobile) {
// Mobile: Stack paths vertically
const currentPathEl = pathInfo.createEl('div', {
cls: 'noteMover-preview-path-mobile noteMover-preview-path-current',
});
currentPathEl.textContent = entry.currentPath;
const arrowEl = pathInfo.createEl('div', {
cls: 'noteMover-preview-arrow-mobile',
});
arrowEl.textContent = '↓';
const targetPathEl = pathInfo.createEl('div', {
cls: 'noteMover-preview-path-mobile noteMover-preview-path-target',
});
targetPathEl.textContent = entry.targetPath;
} else {
// Desktop: Horizontal layout
pathInfo.innerHTML = `
<span class="noteMover-current-path">${entry.currentPath}</span>
<span class="noteMover-arrow">→</span>
<span class="noteMover-target-path">${entry.targetPath}</span>
`;
}
const details = item.createEl('div', {
cls: isMobile
? 'noteMover-preview-item-details noteMover-preview-item-details-mobile'
: 'noteMover-preview-item-details',
});
if (entry.matchedRule) {
const rule = details.createEl('div', {
cls: 'noteMover-preview-item-rule',
});
rule.innerHTML = `<span class="noteMover-rule-label">Rule:</span> <code>${entry.matchedRule}</code>`;
}
if (entry.tags && entry.tags.length > 0) {
const tags = details.createEl('div', {
cls: 'noteMover-preview-item-tags',
});
tags.innerHTML = `<span class="noteMover-tags-label">Tags:</span> ${entry.tags.map(tag => `<span class="noteMover-tag">${tag}</span>`).join(' ')}`;
}
});
}
// createBlockedMovesSection removed - only files that will be moved are shown
private createActionButtons(container: HTMLElement) {
const isMobile = MobileUtils.isMobile();
const footer = container.createEl('div', {
cls: isMobile
? 'noteMover-modal-footer noteMover-modal-footer-mobile'
: 'noteMover-modal-footer',
});
this.actionFooterEl = footer;
const buttonContainer = this.createButtonContainer(footer);
if (isMobile) {
// Mobile: Stack buttons vertically
// Execute moves button (only if there are moves to execute)
if (this.movePreview.successfulMoves.length > 0) {
const executeSetting = new Setting(buttonContainer).addButton(btn => {
btn
.setButtonText(
`Move ${this.movePreview.successfulMoves.length} files`
)
.setCta()
.onClick(() => {
this.executeMoves();
});
});
const executeBtn = executeSetting.settingEl.querySelector('button');
if (executeBtn) {
executeBtn.style.width = '100%';
executeBtn.style.minHeight = '48px';
}
}
// Cancel button
const cancelSetting = new Setting(buttonContainer).addButton(btn => {
btn.setButtonText('Cancel').onClick(() => {
this.close();
});
});
const cancelBtn = cancelSetting.settingEl.querySelector('button');
if (cancelBtn) {
cancelBtn.style.width = '100%';
cancelBtn.style.minHeight = '48px';
}
} else {
// Desktop: Original horizontal layout
// Cancel button
this.createButton(buttonContainer, 'Cancel', () => {
this.close();
});
// Execute moves button (only if there are moves to execute)
if (this.movePreview.successfulMoves.length > 0) {
this.createButton(
buttonContainer,
`Move ${this.movePreview.successfulMoves.length} files`,
() => {
this.executeMoves();
},
{
isPrimary: true,
}
);
}
}
}
private async executeMoves() {
const successfulEntries = this.movePreview.successfulMoves;
let movedCount = 0;
let errorCount = 0;
const abortCtl = new AbortController();
if (this.actionFooterEl) {
this.actionFooterEl.empty();
const status = this.actionFooterEl.createDiv({
cls: 'noteMover-preview-bulk-status',
text: `Moving files… (${successfulEntries.length} planned)`,
});
const row = this.actionFooterEl.createDiv({
cls: 'noteMover-preview-bulk-actions',
});
new Setting(row).addButton(btn =>
btn.setButtonText('Stop').onClick(() => {
abortCtl.abort();
status.setText('Stopping after current file…');
})
);
}
this.plugin.historyManager.startBulkOperation('bulk');
try {
for (let i = 0; i < successfulEntries.length; i++) {
const entry = successfulEntries[i];
if (abortCtl.signal.aborted) {
break;
}
try {
const file = this.app.vault.getAbstractFileByPath(entry.currentPath);
if (!(file instanceof TFile) || !entry.targetPath) continue;
const targetFolder = entry.targetPath;
const newPath = combinePath(targetFolder, file.name);
if (file.path === newPath) continue;
if (!(await ensureFolderExists(this.app, targetFolder))) {
throw createError(
`Failed to create target folder: ${targetFolder}`
);
}
this.plugin.historyManager.markPluginMoveStart();
try {
await this.app.fileManager.renameFile(file, newPath);
this.plugin.historyManager.addEntry({
sourcePath: entry.currentPath,
destinationPath: newPath,
fileName: file.name,
});
movedCount++;
} finally {
this.plugin.historyManager.markPluginMoveEnd();
}
} catch (error) {
handleError(error, `Error moving file ${entry.fileName}`, false);
errorCount++;
}
if ((i + 1) % 50 === 0 && i + 1 < successfulEntries.length) {
await new Promise<void>(resolve => {
const ric = (
globalThis as typeof globalThis & {
requestIdleCallback?: (
cb: IdleRequestCallback,
opts?: IdleRequestOptions
) => number;
}
).requestIdleCallback;
if (typeof ric === 'function') {
ric(() => resolve(), { timeout: 250 });
} else {
setTimeout(resolve, 0);
}
});
}
}
} finally {
this.plugin.historyManager.endBulkOperation();
}
this.close();
if (abortCtl.signal.aborted) {
NoticeManager.info(
`Bulk move stopped. ${movedCount} file(s) moved, ${errorCount} error(s).`
);
} else if (errorCount === 0) {
NoticeManager.success(`Successfully moved ${movedCount} files!`);
} else {
NoticeManager.warning(
`Moved ${movedCount} files with ${errorCount} errors. Check console for details.`
);
}
}
}