<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>File Manager with FTP Sync</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <nav id="sidebar">
      <button id="selectDirBtn" aria-label="Select Directory">📁</button>
      <button id="toggleFilesBtn" aria-label="Toggle Files">📂</button>
      <button id="toggleTrashBtn" aria-label="Toggle Trash">🗑️</button>
      <button id="toggleThemeBtn" aria-label="Toggle Theme">🌓</button>
      <button id="toggleConfirmationsBtn" aria-label="Toggle Confirmations">🔒</button>
      <button id="openSyncSettingsBtn" aria-label="Open FTP Sync Settings">⚙️</button>
      <button id="toggleSyncBtn" aria-label="Toggle Sync">🔄</button>
      <input type="text" id="searchInput" placeholder="Поиск..." aria-label="Search Files and Folders" />
    </nav>

    <section class="wrapper">
      <section id="filePanel" class="panel left hidden" role="region">
        <button id="createFolderBtn" aria-label="Create Folder">Создать папку</button>
        <div id="foldersList" class="droppable" role="list"></div>
      </section>

      <div class="gutter hidden" id="resizer1"></div>

      <section id="folderContentPanel" class="panel middle hidden" role="region">
        <button id="createFileBtn" aria-label="Create File">Создать файл</button>
        <div id="filesList" class="droppable" role="list"></div>
      </section>

      <div class="gutter hidden" id="resizer2"></div>

      <section id="trashPanel" class="panel left hidden" role="region">
        <button id="restoreFromTrashBtn" aria-label="Restore from Trash">Восстановить</button>
        <div id="trashList" class="droppable" role="list"></div>
      </section>

      <div class="gutter hidden" id="resizer3"></div>

      <section id="thirdPanel" class="panel right">
        <div id="contentView">FTP синхронизация</div>
      </section>
    </section>

    <div id="infoDisplay" role="status"></div>

    <div id="contextMenu" class="context-menu">
      <button id="openOption">Открыть</button>
      <button id="renameOption">Переименовать</button>
      <button id="deleteOption">Удалить</button>
      <button id="restoreOption">Восстановить</button>
      <button id="syncOption">Синхронизировать/Отменить синхронизацию</button>
    </div>

    <!-- Диалоговое окно настроек синхронизации -->
    <div id="syncSettingsDialog" class="confirmation-dialog hidden">
      <h2>Настройки синхронизации FTP</h2>
      <div>
        <label>
          Подключения FTP:
          <select id="ftpConnections"></select>
        </label>
        <button id="addFtpConnectionBtn">Добавить новое подключение</button>
      </div>
      <div id="ftpConnectionDetails" class="hidden">
        <label> Хост: <input type="text" id="ftpHostInput" /> </label>
        <label> Порт: <input type="number" id="ftpPortInput" value="21" /> </label>
        <label> Имя пользователя: <input type="text" id="ftpUserInput" /> </label>
        <label> Пароль: <input type="password" id="ftpPasswordInput" /> </label>
        <label> Удаленный каталог: <input type="text" id="remoteDirInput" /> </label>
      </div>
      <div>
        <h3>Опции синхронизации</h3>
        <label>
          <input type="checkbox" id="syncOnStartCheckbox" />
          Синхронизация при старте
        </label>
        <label>
          <input type="checkbox" id="syncOnChangesCheckbox" />
          Синхронизация при изменении
        </label>
        <label> Таймер синхронизации (минуты): <input type="number" id="syncIntervalInput" /> </label>
      </div>
      <div>
        <button id="saveFtpSettingsBtn">Сохранить</button>
        <button id="closeSyncSettingsBtn">Отмена</button>
      </div>
    </div>
    <div id="confirmationDialog" class="confirmation-dialog hidden">
      <p id="confirmationMessage"></p>
      <!-- Сообщение подтверждения -->
      <div>
        <button id="confirmYes">Да</button>
        <!-- Кнопка подтверждения -->
        <button id="confirmNo">Нет</button>
        <!-- Кнопка отмены -->
      </div>
    </div>
    <!-- Диалог выбора при конфликте -->
    <div id="fileConflictDialog" class="confirmation-dialog hidden">
      <h2>Конфликты синхронизации</h2>
      <p>Выберите действие для каждого конфликта:</p>
      <table>
        <thead>
          <tr>
            <th>Имя файла</th>
            <th>Локальный</th>
            <th>Удалённый</th>
            <th>Действие</th>
          </tr>
        </thead>
        <tbody id="conflictTable"></tbody>
      </table>
      <button id="resolveConflictsBtn">Применить</button>
    </div>
    <script src="js/app.js" type="module"></script>
  </body>
</html>


/* Основные переменные цветов для лёгкой настройки */
:root {
  --primary-color: #2c3e50; /* Основной цвет боковой панели */
  --secondary-color: #ecf0f1; /* Лёгкий цвет для текста и фона */
  --highlight-color: #3498db; /* Цвет выделения */
  --success-color: #2ecc71; /* Зелёный для успешных операций */
  --error-color: #e74c3c; /* Красный для ошибок */
  --text-color: #333333; /* Основной цвет текста */
  --dark-bg-color: #1e1e1e; /* Тёмный фон (в тёмной теме) */
  --dark-text-color: #ffffff; /* Текст при тёмной теме */
  --divider-color: #dcdcdc; /* Разделитель между панелями */
  --gutter-color: #7f8c8d; /* Цвет линии ресайзера */
}

*,
*::before,
*::after {
  box-sizing: border-box; /* Корректное отображение размеров элементов */
  margin: 0;
  padding: 0;
}

body {
  margin: 0;
  font-family: Arial, sans-serif; /* Удобный шрифт без засечек */
  font-size: 14px;
  color: var(--text-color); /* Основной цвет текста */
  background-color: var(--secondary-color); /* Фон приложения */
  display: flex;
  height: 100vh; /* Высота окна */
}

body.dark-mode {
  background-color: var(--dark-bg-color); /* Тёмная тема — фон */
  color: var(--dark-text-color); /* Тёмная тема — текст */
}

/* --- Боковая панель --- */
#sidebar {
  width: 60px; /* Фиксированная ширина боковой панели */
  background-color: var(--primary-color); /* Основной цвет */
  color: var(--secondary-color); /* Текст на боковой панели */
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px 0;
  gap: 10px; /* Расстояние между кнопками */
}

#sidebar button,
#sidebar input {
  background: none;
  border: none;
  color: var(--secondary-color); /* Цвет текста */
  font-size: 20px;
  cursor: pointer;
}

#sidebar input {
  background-color: rgba(255, 255, 255, 0.2); /* Полупрозрачный фон */
  color: var(--secondary-color);
  padding: 5px;
  border-radius: 5px;
  font-size: 14px;
}

#sidebar button:hover,
#sidebar input:focus {
  color: var(--highlight-color); /* Цвет при наведении и фокусе */
}

/* --- Общая структура панелей --- */
.wrapper {
  height: 100%;
  width: calc(100% - 60px); /* Вычитаем ширину боковой панели */
  display: flex;
}

.panel {
  background: var(--secondary-color); /* Фон панелей */
  padding: 15px;
  overflow-y: auto;
  border-right: 1px solid var(--divider-color); /* Разделительная линия */
}

.panel.hidden {
  display: none;
}

/* --- Разделители между панелями --- */
.gutter {
  width: 4px;
  height: 100%;
  background: var(--gutter-color); /* Цвет разделителей */
  cursor: ew-resize;
}
.gutter.hidden {
  display: none;
}

/* --- Элементы файлов и папок --- */
.draggable {
  display: flex;
  align-items: center;
  margin: 5px 0;
  padding: 8px 10px;
  border-radius: 4px;
  background: none;
  cursor: pointer;
}

.draggable:hover {
  background: rgba(52, 152, 219, 0.2); /* Лёгкий синий фон при наведении */
}

.draggable::before {
  content: '';
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-right: 8px; /* Отступ между кружком и текстом */
  border-radius: 50%;
  background-color: transparent; /* Прозрачный по умолчанию */
}

.draggable.synced::before {
  background-color: var(--success-color); /* Зелёный кружок для синхронизации */
}

.draggable.unsynced::before {
  background-color: var(--error-color); /* Красный кружок для не синхронизированных */
}

/* --- Диалог подтверждения --- */
.confirmation-dialog {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: var(--secondary-color); /* Белый фон */
  border: 1px solid var(--divider-color);
  padding: 20px;
  border-radius: 8px; /* Закруглённые края */
  z-index: 1001;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25); /* Тень */
}

.confirmation-dialog.hidden {
  display: none;
}

.confirmation-dialog button {
  margin: 5px;
  padding: 8px 15px;
  background-color: var(--highlight-color); /* Синий фон кнопок */
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.confirmation-dialog button:hover {
  background-color: darken(var(--highlight-color), 10%);
}

.confirmation-dialog #confirmNo {
  background-color: var(--error-color); /* Красный для отмены */
}

/* --- Контекстное меню --- */
.context-menu {
  position: absolute;
  display: none;
  background: white;
  border: 1px solid var(--divider-color);
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); /* Тень меню */
  z-index: 1000;
}

.context-menu.visible {
  display: block;
}

.context-menu button {
  display: block;
  padding: 10px;
  width: 100%;
  border: none;
  background: none;
  cursor: pointer;
}

.context-menu button:hover {
  background: rgba(0, 0, 0, 0.1); /* Тёмный фон при наведении */
}

/* --- Информационное положение --- */
#infoDisplay {
  position: fixed;
  bottom: 10px;
  right: 10px;
  background: rgba(52, 73, 94, 0.9); /* Полупрозрачный фон */
  color: white;
  padding: 10px 15px;
  border-radius: 5px;
  font-size: 14px;
}

/* --- Тёмная тема (дополнительно) --- */
body.dark-mode .panel {
  background: var(--dark-bg-color);
  color: var(--dark-text-color);
}

body.dark-mode #infoDisplay {
  background: rgba(255, 255, 255, 0.2);
  color: #eee;
}
.panel.resizing {
  cursor: col-resize;
  border-right: 3px dashed var(--highlight-color); /* Подсветка */
  transition: border-color 0.2s ease-in-out;
}



Файл app.js:


import * as ui from './ui.js';
import * as settings from './settings.js';
import * as files from './files.js';
import * as ftp from './ftp.js';
import * as sync from './sync.js';
import * as dialogs from './dialogs.js';

// Инициализация приложения
async function initApp() {
  try {
    await settings.loadSettings(); // Загрузка настроек
    ui.initUI(); // Инициализация UI
    files.initFiles(ui.displayInfo, ui.refreshUI);
    ftp.initFtp(ui.displayInfo);
    sync.initSync(
      ui.displayInfo,
      ftp.safelyConnectToFtp,
      ftp.safelyCloseFtp,
      ui.updateSyncIndicators,
      files.getAppPath()
    );

    // После загрузки настроек, восстановить ширину панелей
    ui.setPanelWidths(settings.getPanelWidths());
    ui.updateThirdPanelWidth();

    //  Привязываем обработку ресайзеров
    ui.initResizers(settings.saveSettings);

    // Загрузка данных для синхронизации
    await sync.loadSyncData();
    ui.updateSyncIndicators(sync.getFilesForSync());

    // Загрузка папок и корзины
    await ui.refreshUI();

    // Запускаем синхронизацию при старте, если это настроено
    if (settings.getSyncOptions().syncOnStart) {
      await sync.startSyncOnStart();
    }

    // Запускаем интервал синхронизации, если это настроено
    if (settings.getSyncOptions().syncInterval) {
      sync.startSyncInterval(settings.getSyncOptions().syncInterval);
    }

    // Начинаем отслеживание изменений файлов
    if (settings.getSyncOptions().syncOnChanges) {
      sync.startFileWatcher();
    }

    ui.displayInfo('Приложение успешно запущено.');
  } catch (err) {
    console.error('Ошибка при запуске приложения:', err);
    ui.displayInfo('Ошибка при запуске приложения. Проверьте настройки.');
  }
}

// Обработчик события при закрытии окна
window.addEventListener('beforeunload', () => {
  sync.cleanUpSync();
  ui.displayInfo('Закрыты все ресурсы. Завершение приложения.');
});

// Запуск приложения
document.addEventListener('DOMContentLoaded', initApp);

// Обработчики событий для UI
document.getElementById('selectDirBtn').addEventListener('click', files.selectDirectory);
document.getElementById('toggleFilesBtn').addEventListener('click', ui.toggleFilePanel);
document.getElementById('toggleTrashBtn').addEventListener('click', ui.toggleTrashPanel);
document.getElementById('toggleThemeBtn').addEventListener('click', settings.toggleTheme);
document.getElementById('toggleConfirmationsBtn').addEventListener('click', settings.toggleConfirmations);
document.getElementById('openSyncSettingsBtn').addEventListener('click', ui.openSyncSettings);
document.getElementById('closeSyncSettingsBtn').addEventListener('click', ui.closeSyncSettings);
document.getElementById('saveFtpSettingsBtn').addEventListener('click', ftp.saveCurrentFtpConnection);
document.getElementById('addFtpConnectionBtn').addEventListener('click', ftp.addNewFtpConnection);
document.getElementById('ftpConnections').addEventListener('change', ftp.selectFtpConnection);
document.getElementById('toggleSyncBtn').addEventListener('click', sync.toggleSync);
document.getElementById('createFolderBtn').addEventListener('click', files.createFolder);
document.getElementById('createFileBtn').addEventListener('click', files.createFile);

document.addEventListener('click', (e) => {
  if (e.target.matches("button[data-action='local'], button[data-action='remote']")) {
    dialogs.highlightConflictSelection(e);
  }
});


Файл dialogs.js:


export function showConfirmationDialog(message, onConfirm) {
  const dialog = document.getElementById('confirmationDialog');
  const confirmationMessage = document.getElementById('confirmationMessage');

  if (!confirmationMessage || !dialog) {
    console.error('Элемент подтверждения отсутствует в DOM.');
    return;
  }

  confirmationMessage.textContent = message;

  const confirmYes = document.getElementById('confirmYes');
  const confirmNo = document.getElementById('confirmNo');

  if (!confirmYes || !confirmNo) {
    console.error('Кнопки подтверждения отсутствуют в DOM.');
    return;
  }

  confirmYes.onclick = () => {
    onConfirm();
    closeConfirmationDialog();
  };

  confirmNo.onclick = closeConfirmationDialog;

  dialog.classList.remove('hidden');
}

export function closeConfirmationDialog() {
  const dialog = document.getElementById('confirmationDialog');
  if (dialog) {
    dialog.classList.add('hidden');
  }
}

export function highlightConflictSelection(e) {
  if (e.target.matches("button[data-action='local'], button[data-action='remote']")) {
    const buttons = e.target.closest('td').querySelectorAll('button');
    buttons.forEach((btn) => btn.classList.remove('selected'));
    e.target.classList.add('selected');
  }
}

export function resolveConflicts() {
  const conflictTable = document.getElementById('conflictTable');
  conflictTable.innerHTML = '';

  conflicts.forEach((conflict) => {
    const row = document.createElement('tr');
    row.innerHTML = `
      <td>${conflict.file.name}</td>
      <td><button data-action="local" class="btn-action">Локальный</button></td>
      <td><button data-action="remote" class="btn-action">Удалённый</button></td>
      <td><button class="btn-action resolve" data-path="${conflict.file.path}" data-remote="${conflict.remotePath}">Решить</button></td>
    `;
    conflictTable.appendChild(row);

    row.querySelector('.resolve').addEventListener('click', (e) => {
      const path = e.target.getAttribute('data-path');
      const remotePath = e.target.getAttribute('data-remote');
      const action = row.querySelector('.btn-action.selected').dataset.action;

      if (action === 'local') {
        tasks.push({ type: 'upload', localPath: path, remotePath });
      } else if (action === 'remote') {
        tasks.push({ type: 'download', localPath: path, remotePath });
      }

      conflicts = conflicts.filter((c) => c.file.path !== path);
      row.remove();
    });
  });
}

export async function resolveFileConflict(file, resolution) {
  // Проверяем, выбрал ли пользователь решение для конфликта
  if (!resolution) {
    displayInfo(`Не выбрано действие для файла: ${file.file.name}`);
    return;
  }

  try {
    const client = await import('./ftp.js').then((module) =>
      module.safelyConnectToFtp(module.getCurrentFtpConnection())
    );
    if (!client) {
      throw new Error('FTP клиент не подключен');
    }

    if (resolution === 'local') {
      // Загружаем локальный файл на сервер
      displayInfo(`Загружаем локальный файл для разрешения конфликта: ${file.file.name}`);
      await client.uploadFrom(file.file.path, file.remotePath);
    } else if (resolution === 'remote') {
      // Загружаем удаленный файл локально
      displayInfo(`Загружаем удаленный файл для разрешения конфликта: ${file.file.name}`);
      await client.downloadTo(file.file.path, file.remotePath);
    }

    // Удаляем файл из конфликта
    conflicts = conflicts.filter((c) => c.file.path !== file.file.path);
    displayInfo(`Файл "${file.file.name}" успешно синхронизирован и конфликт разрешен.`);
    client.close();
  } catch (err) {
    console.error(`Ошибка решения конфликта для файла "${file.file.name}":`, err);
    displayInfo(`Ошибка решения конфликта: ${file.file.name}`);
  }
}

document.getElementById('resolveConflictsBtn').addEventListener('click', () => {
  const rows = Array.from(document.querySelectorAll('#conflictTable tr'));

  rows.forEach((row) => {
    const filePath = row.dataset.filePath;
    const resolution = row.querySelector('.btn-action.selected')?.dataset.action;

    const conflict = conflicts.find((c) => c.file.path === filePath);
    if (conflict) {
      resolveFileConflict(conflict, resolution);
    }
  });

  // Закрыть диалог после обработки
  document.getElementById('fileConflictDialog').classList.add('hidden');
});


Файл files.js:


import * as dialogs from './dialogs.js';
const fs = require('fs').promises;
const path = require('path');
const appPath = nw.__dirname;

let currentDir = appPath;
let filesForSync = [];

let displayInfo;
let refreshUI;

export function initFiles(displayInfoCallback, refreshUICallback) {
  displayInfo = displayInfoCallback;
  refreshUI = refreshUICallback;
}

export function getAppPath() {
  return appPath;
}

export function getCurrentDir() {
  return currentDir;
}

export function setCurrentDir(dir) {
  currentDir = dir;
}

export async function readDir(dir, options) {
  try {
    await fs.access(dir); // Проверяем доступность директории
    return await fs.readdir(dir, options); // Читаем содержимое
  } catch (err) {
    console.error('Ошибка чтения директории:', err);
    throw err;
  }
}

export async function createDirectoryIfNotExists(dirPath) {
  try {
    await fs.access(dirPath);
  } catch (err) {
    if (err.code === 'ENOENT') {
      await fs.mkdir(dirPath, { recursive: true });
    } else {
      throw err;
    }
  }
}

export async function loadTrash() {
  try {
    const saveData = await loadSaveData();
    return saveData.trash || [];
  } catch (err) {
    console.error('Ошибка загрузки корзины:', err);
    return [];
  }
}

export async function updateSaveDataWithTrashItem(trashItem) {
  let saveData = await loadSaveData();
  saveData.trash.push(trashItem);
  await saveSaveData(saveData);
}

export async function renameItem(itemPath, itemType) {
  const itemName = path.basename(itemPath);
  const newName = prompt(`Введите новое имя для ${itemType}:`, itemName);
  if (!newName || newName === itemName) {
    displayInfo('Переименование отменено.');
    return;
  }
  const newPath = path.join(path.dirname(itemPath), newName);

  try {
    await fs.rename(itemPath, newPath);
    displayInfo(`"${itemName}" успешно переименован в "${newName}".`);
    await refreshUI();
  } catch (err) {
    console.error(`Ошибка переименования объекта "${itemName}":`, err);
    if (err.code === 'EEXIST') {
      displayInfo(`Объект с именем "${newName}" уже существует.`);
    } else {
      displayInfo(`Ошибка переименования "${itemName}": ${err.message}`);
    }
  }
}

export function getTrashPath() {
  return path.join(appPath, 'Trash');
}

export async function deleteFromTrash(itemName) {
  try {
    let saveData = await loadSaveData();
    const trashItemIndex = saveData.trash.findIndex((item) => item.name === itemName);
    if (trashItemIndex !== -1) {
      const trashItem = saveData.trash[trashItemIndex];
      await fs.access(trashItem.trashPath, fs.constants.F_OK);

      if (trashItem.isFile) {
        await fs.unlink(trashItem.trashPath);
      } else {
        await fs.rmdir(trashItem.trashPath, { recursive: true });
      }

      saveData.trash.splice(trashItemIndex, 1);
      await saveSaveData(saveData);
      displayInfo(`Элемент "${itemName}" удален из корзины.`);
      await refreshUI();
    }
  } catch (err) {
    console.error('Не удалось удалить элемент из корзины:', err);
    displayInfo(`Ошибка удаления элемента "${itemName}" из корзины.`);
  }
}

export async function createFolder() {
  const folderName = prompt('Введите имя новой папки:');
  if (!folderName || folderName.trim() === '') {
    displayInfo('Операция создания папки отменена или имя папки пустое.');
    return;
  }
  if (folderName) {
    const folderPath = path.join(currentDir, folderName);
    try {
      await fs.access(folderPath);
      alert('Папка с таким именем уже существует.');
    } catch {
      await fs.mkdir(folderPath);
      let saveData = await loadSaveData();
      saveData.folders.push({ name: folderName, path: folderPath });
      await saveSaveData(saveData);
      await refreshUI();
      displayInfo(`Папка "${folderName}" создана.`);
    }
  }
}

export async function createFile() {
  const currentFolder = document.getElementById('folderContentPanel').getAttribute('data-current-folder');
  if (currentFolder) {
    const fileName = prompt('Введите имя нового файла:');
    if (fileName) {
      const filePath = path.join(currentDir, currentFolder, fileName);
      try {
        await fs.access(filePath);
        alert('Файл с таким именем уже существует.');
      } catch {
        await fs.writeFile(filePath, '');
        let saveData = await loadSaveData();
        saveData.files.push({ name: fileName, path: filePath });
        await saveSaveData(saveData);
        await refreshUI();
        displayInfo(`Файл "${fileName}" создан.`);
      }
    }
  } else {
    alert('Пожалуйста, выберите папку для создания файла.');
  }
}

export async function moveToTrash(itemPath, type) {
  try {
    const itemStats = await fs.lstat(itemPath);
    const itemName = path.basename(itemPath);
    const trashDir = getTrashPath();

    await createDirectoryIfNotExists(trashDir);

    const trashItemPath = path.join(trashDir, itemName);
    const trashItem = {
      name: itemName,
      originalPath: itemPath,
      trashPath: trashItemPath,
      type,
      isFile: itemStats.isFile(),
    };

    const confirmDeletion = await import('./settings.js').then((module) => module.getConfirmDeletion());

    if (confirmDeletion) {
      dialogs.showConfirmationDialog(`Вы уверены, что хотите переместить "${itemName}" в корзину?`, async () => {
        await updateSaveDataWithTrashItem(trashItem);
        await fs.rename(itemPath, trashItemPath);
        displayInfo(`Элемент "${itemName}" перемещен в корзину.`);
        if (type === 'folder' && itemName === document.getElementById('folderContentPanel').dataset.currentFolder) {
          document.getElementById('folderContentPanel').classList.add('hidden');
          document.getElementById('resizer2').classList.add('hidden');
          document.getElementById('folderContentPanel').removeAttribute('data-current-folder');
        }
        await refreshUI();
      });
    } else {
      await updateSaveDataWithTrashItem(trashItem);
      await fs.rename(itemPath, trashItemPath);
      displayInfo(`Элемент "${itemName}" перемещен в корзину.`);
      if (type === 'folder' && itemName === document.getElementById('folderContentPanel').dataset.currentFolder) {
        document.getElementById('folderContentPanel').classList.add('hidden');
        document.getElementById('resizer2').classList.add('hidden');
        document.getElementById('folderContentPanel').removeAttribute('data-current-folder');
      }
      await refreshUI();
    }
  } catch (err) {
    console.error(`Не удалось переместить элемент "${itemPath}" в корзину:`, err);
    displayInfo(`Ошибка перемещения элемента "${itemPath}" в корзину: ${err}`);
  }
}

export async function restoreFromTrash(itemName) {
  try {
    let saveData = await loadSaveData();
    const trashItemIndex = saveData.trash.findIndex((item) => item.name === itemName);
    if (trashItemIndex !== -1) {
      const trashItem = saveData.trash[trashItemIndex];
      const targetPath = trashItem.originalPath;

      const confirmDeletion = await import('./settings.js').then((module) => module.getConfirmDeletion());
      if (confirmDeletion) {
        dialogs.showConfirmationDialog(`Вы уверены, что хотите восстановить "${itemName}" из корзины?`, async () => {
          await fs.rename(trashItem.trashPath, targetPath);
          saveData.trash.splice(trashItemIndex, 1);
          await saveSaveData(saveData);
          displayInfo(`Элемент "${itemName}" восстановлен из корзины.`);
          await refreshUI();
        });
      } else {
        await fs.rename(trashItem.trashPath, targetPath);
        saveData.trash.splice(trashItemIndex, 1);
        await saveSaveData(saveData);
        displayInfo(`Элемент "${itemName}" восстановлен из корзины.`);
        await refreshUI();
      }
    }
  } catch (err) {
    console.error('Не удалось восстановить элемент из корзины:', err);
    displayInfo(`Ошибка восстановления элемента "${itemName}" из корзины.`);
  }
}

export async function selectDirectory() {
  const fileInput = document.createElement('input');
  fileInput.type = 'file';
  fileInput.setAttribute('nwdirectory', true);

  fileInput.addEventListener('change', async () => {
    if (fileInput.files.length > 0) {
      currentDir = fileInput.files[0].path;
      await import('./settings.js').then((module) => module.saveSettings());
      await refreshUI();
      displayInfo(`Выбрана директория: ${currentDir}`);
    }
  });

  fileInput.click();
}

// --- УПРАВЛЕНИЕ ОБНОВЛЕНИЕМ ДАННЫХ В ПРИЛОЖЕНИИ ---
export async function loadSaveData() {
  try {
    const data = await fs.readFile('save.json', 'utf8');
    return JSON.parse(data) || { folders: [], files: [], trash: [], syncFiles: [] };
  } catch (err) {
    if (err.code === 'ENOENT') {
      await saveSaveData({ folders: [], files: [], trash: [], syncFiles: [] });
      return { folders: [], files: [], trash: [], syncFiles: [] };
    } else {
      console.error('Не удалось загрузить данные сохранения:', err);
      throw err;
    }
  }
}

export async function saveSaveData(data) {
  try {
    await fs.writeFile('save.json', JSON.stringify(data, null, 2));
    displayInfo('Данные сохранены.');
  } catch (err) {
    console.error('Не удалось сохранить данные:', err);
    displayInfo('Ошибка сохранения данных.');
  }
}

export async function toggleSyncStatus(itemPath, itemType, itemElement) {
  if (!itemPath || !itemType) {
    console.error('Отсутствует атрибут itemPath или itemType. Не удалось переключить статус синхронизации.');
    return;
  }

  const isSynced = filesForSync.some((file) => file.path === itemPath);

  if (isSynced) {
    // Удаляем из синхронизации сам объект И ВСЕ ЕГО СОДЕРЖИМОЕ
    const parentPath = itemPath;

    filesForSync = filesForSync.filter((file) => {
      const isChild = file.path.startsWith(parentPath);
      if (isChild) {
        console.log(`Удаление из синхронизации: ${file.path}`);
      }
      return !isChild; // Фильтруем только элементы, которые не являются дочерними
    });

    // Обновляем все подпатчи: элементы, которые больше не нужны
    const folderItems = document.querySelectorAll(`[data-path^="${itemPath}"]`);
    folderItems.forEach((child) => {
      child.classList.remove('synced', 'conflict');
      child.classList.add('unsynced');
    });

    itemElement.classList.remove('synced');
    itemElement.classList.add('unsynced');
    displayInfo(`${itemType} "${itemPath}" удалён из синхронизации, а также его содержимое.`);
  } else {
    // Если объект не синхронизируется, добавляем рекурсивно содержимое (если папка)
    if (itemType === 'folder') {
      await addFolderToSync(itemPath); // Сюда нужно передать значение для рекурсивного добавления
    } else {
      filesForSync.push({ path: itemPath, type: itemType, name: path.basename(itemPath) });
    }

    itemElement.classList.remove('unsynced');
    itemElement.classList.add('synced');
    displayInfo(`${itemType} "${itemPath}" добавлен в синхронизацию.`);
  }

  await saveSyncData(); // Перезаписываем данные синхронизации
}
// Рекурсивное добавление содержимого папки в список для синхронизации
export async function addFolderToSync(folderPath) {
  try {
    const items = await readDir(folderPath, { withFileTypes: true });
    if (!items || items.length === 0) {
      displayInfo(`Папка "${path.basename(folderPath)}" пуста.`);
    }

    // Добавляем саму папку в список синхронизации
    filesForSync.push({ path: folderPath, type: 'folder', name: path.basename(folderPath) });

    // Рекурсивно обрабатываем файлы и папки
    for (const item of items) {
      const itemPath = path.join(folderPath, item.name);
      if (item.isDirectory()) {
        await addFolderToSync(itemPath);
      } else if (item.isFile()) {
        filesForSync.push({ path: itemPath, type: 'file', name: item.name });
      }
    }
  } catch (err) {
    console.error(`Ошибка при добавлении папки "${folderPath}" в синхронизацию:`, err);
    displayInfo(`Не удалось обработать папку "${path.basename(folderPath)}".`);
  }
}

export async function saveSyncData() {
  let saveData = await loadSaveData();
  saveData.syncFiles = filesForSync.map((file) => ({
    path: file.path,
    type: file.type,
    name: file.name,
  }));

  try {
    await saveSaveData(saveData);
    displayInfo('Обновлён список файлов синхронизации.');
  } catch (err) {
    console.error('Ошибка сохранения списка файлов синхронизации:', err);
    displayInfo('Ошибка сохранения списка синхронизации.');
  }
}

export function getFilesForSync() {
  return filesForSync;
}

export function setFilesForSync(files) {
  filesForSync = files;
}

export async function stat(filePath) {
  try {
    return await fs.stat(filePath);
  } catch (error) {
    console.error(`Ошибка получения информации о файле: ${filePath}`, error);
    throw error;
  }
}


Файл ftp.js:


import * as ui from './ui.js';
const ftp = require('basic-ftp');
const fs = require('fs').promises;

let ftpConnections = [];
let currentFtpConnection = null;
let displayInfo;

export function initFtp(displayInfoCallback) {
  displayInfo = displayInfoCallback;
  loadFtpConnections();
}

export async function loadFtpConnections() {
  try {
    const data = await fs.readFile('settings.json', 'utf8');
    const parsedData = JSON.parse(data);
    ftpConnections = parsedData.ftpConnections || [];
    updateFtpConnectionList();
  } catch (err) {
    console.error('Не удалось загрузить FTP-подключения:', err);
    ftpConnections = [];
  }
}

async function saveFtpConnections() {
  try {
    const settingsData = await fs.readFile('settings.json', 'utf8');
    const settings = JSON.parse(settingsData);
    settings.ftpConnections = ftpConnections;
    await fs.writeFile('settings.json', JSON.stringify(settings, null, 2));
  } catch (err) {
    console.error('Не удалось сохранить FTP-подключения:', err);
  }
}

export async function safelyConnectToFtp(connection) {
  const client = new ftp.Client();
  client.ftp.verbose = true;
  try {
    await client.access({
      host: connection.host,
      port: connection.port,
      user: connection.user || '',
      password: connection.password || '',
      secure: connection.secure || false, // Теперь поддержка защищенного соединения
    });
    displayInfo(`Успешно подключено к FTP (${connection.secure ? 'защищенное' : 'незащищенное'} соединение).`);
    return client;
  } catch (err) {
    console.error('Ошибка подключения к FTP:', err);
    displayInfo('Ошибка подключения к FTP: ' + err.message);
    return null;
  }
}

export async function safelyCloseFtp(client) {
  if (client) {
    try {
      client.close();
    } catch (err) {
      console.error('Ошибка закрытия FTP-соединения:', err);
    }
  }
}

export function populateFtpConnectionFields(connection) {
  document.getElementById('ftpHostInput').value = connection.host || '';
  document.getElementById('ftpPortInput').value = connection.port || '21';
  document.getElementById('ftpUserInput').value = connection.user || '';
  document.getElementById('ftpPasswordInput').value = connection.password || '';
  document.getElementById('remoteDirInput').value = connection.remoteDir || '/';
}

export async function saveCurrentFtpConnection() {
  if (!currentFtpConnection) {
    displayInfo('Нет активного подключения для сохранения.');
    return;
  }

  const host = document.getElementById('ftpHostInput').value.trim();
  const port = parseInt(document.getElementById('ftpPortInput').value, 10);

  if (!host) {
    displayInfo('Хост не может быть пустым.');
    return;
  }

  if (isNaN(port) || port <= 0) {
    displayInfo('Введите корректный номер порта.');
    return;
  }

  currentFtpConnection.host = host;
  currentFtpConnection.port = port;
  currentFtpConnection.user = document.getElementById('ftpUserInput').value;
  currentFtpConnection.password = document.getElementById('ftpPasswordInput').value;
  currentFtpConnection.remoteDir = document.getElementById('remoteDirInput').value;

  updateFtpConnectionList();
  await saveFtpConnections();
  displayInfo('Настройки текущего подключения FTP успешно сохранены.');
}

export function updateFtpConnectionList() {
  const dropdown = document.getElementById('ftpConnections');
  dropdown.innerHTML = '';

  ftpConnections.forEach((connection, index) => {
    const option = document.createElement('option');
    option.value = index;
    option.textContent = `${connection.host}:${connection.port}`;
    dropdown.appendChild(option);
  });

  if (ftpConnections.length > 0) {
    currentFtpConnection = ftpConnections[0];
    dropdown.value = 0;
    populateFtpConnectionFields(currentFtpConnection);
  }
}

export function selectFtpConnection(e) {
  const selectedIndex = e.target.value;
  currentFtpConnection = ftpConnections[selectedIndex];
  populateFtpConnectionFields(currentFtpConnection);
}

export function addNewFtpConnection() {
  const newConnection = { host: '', port: '21', user: '', password: '', remoteDir: '/' };
  ftpConnections.push(newConnection);
  updateFtpConnectionList();
  const lastIndex = ftpConnections.length - 1;
  document.getElementById('ftpConnections').value = lastIndex;
  populateFtpConnectionFields(newConnection);
}

export function getCurrentFtpConnection() {
  return currentFtpConnection;
}



Файл settings.js:


import * as ui from './ui.js';
import * as files from './files.js';
const fs = require('fs').promises;

let currentDir = files.getAppPath();
let filePanelWidth;
let trashPanelWidth;
let folderContentPanelWidth;
let isDarkMode = false;
let confirmDeletion = true;
let syncOptions = {
  syncOnStart: false,
  syncOnChanges: false,
  syncInterval: null,
};

export async function loadSettings() {
  try {
    const data = await fs.readFile('settings.json', 'utf8');
    const parsedData = JSON.parse(data);
    currentDir = parsedData.currentDir || currentDir;
    filePanelWidth = parsedData.filePanelWidth || filePanelWidth;
    folderContentPanelWidth = parsedData.folderContentPanelWidth || folderContentPanelWidth;
    trashPanelWidth = parsedData.trashPanelWidth || trashPanelWidth;
    isDarkMode = parsedData.isDarkMode || isDarkMode;
    if (parsedData.confirmDeletion !== undefined) {
      confirmDeletion = parsedData.confirmDeletion;
    }
    if (parsedData.syncOptions) {
      syncOptions = parsedData.syncOptions;
    }
    if (isDarkMode) {
      document.body.classList.add('dark-mode');
    }
    files.setCurrentDir(currentDir);
  } catch (err) {
    console.error('Не удалось загрузить настройки:', err);
  }
}

export async function saveSettings() {
  const settings = {
    currentDir,
    filePanelWidth: document.getElementById('filePanel').offsetWidth,
    folderContentPanelWidth: document.getElementById('folderContentPanel').offsetWidth,
    trashPanelWidth: document.getElementById('trashPanel').offsetWidth,
    isDarkMode,
    confirmDeletion,
    syncOptions,
  };

  try {
    await fs.writeFile('settings.json', JSON.stringify(settings, null, 2));
  } catch (err) {
    console.error('Не удалось сохранить настройки:', err);
  }
}

export function getPanelWidths() {
  return { filePanelWidth, folderContentPanelWidth, trashPanelWidth };
}

export function getSyncOptions() {
  return syncOptions;
}

export function toggleTheme() {
  isDarkMode = !isDarkMode;
  document.body.classList.toggle('dark-mode', isDarkMode);
  saveSettings();
  ui.displayInfo(`Тёмный режим ${isDarkMode ? 'включён' : 'выключен'}.`);
}

export async function toggleConfirmations() {
  confirmDeletion = !confirmDeletion;
  await saveSettings();
  ui.displayInfo(`Подтверждение удаления ${confirmDeletion ? 'включено' : 'выключено'}.`);
}

export function getConfirmDeletion() {
  return confirmDeletion;
}


Файл sync.js:


import * as files from './files.js';

const path = require('path');

let fileWatcher = null;
let syncIntervalId = null;
let syncOptions;

let filesForSync = [];
let conflicts = [];
const failedTasks = [];
let safelyConnectToFtp;
let safelyCloseFtp;
let displayInfo;
let updateSyncIndicators;
let appPath;

export function initSync(
  displayInfoCallback,
  safelyConnectToFtpCallback,
  safelyCloseFtpCallback,
  updateSyncIndicatorsCallback,
  appPathCallback
) {
  displayInfo = displayInfoCallback;
  safelyConnectToFtp = safelyConnectToFtpCallback;
  safelyCloseFtp = safelyCloseFtpCallback;
  updateSyncIndicators = updateSyncIndicatorsCallback;
  appPath = appPathCallback;
}

export async function loadSyncData() {
  try {
    const saveData = await files.loadSaveData();
    if (saveData.syncFiles) {
      filesForSync = saveData.syncFiles.map((file) => ({
        path: file.path,
        type: file.type,
        name: file.name,
      }));
    }
    syncOptions = await import('./settings.js').then((module) => module.getSyncOptions());
  } catch (err) {
    console.error('Ошибка загрузки данных синхронизации', err);
    filesForSync = [];
  }
}

export async function startSyncOnStart() {
  displayInfo('Запуск автоматической синхронизации при старте...');
  const currentFtpConnection = await import('./ftp.js').then((module) => module.getCurrentFtpConnection());
  if (currentFtpConnection) {
    await synchronizeFiles();
  } else {
    displayInfo('Нет активного FTP-подключения. Синхронизация не выполнена.');
  }
}

export function getFilesForSync() {
  return filesForSync;
}

export function startSyncInterval(intervalMinutes) {
  if (syncIntervalId) clearInterval(syncIntervalId);

  syncIntervalId = setInterval(async () => {
    const currentFtpConnection = await import('./ftp.js').then((module) => module.getCurrentFtpConnection());
    if (!currentFtpConnection) {
      displayInfo('Нет активного FTP-подключения для интервала синхронизации.');
      return;
    }
    displayInfo('Запущена синхронизация по таймеру...');
    await synchronizeFiles();
  }, intervalMinutes * 60 * 1000);
  displayInfo(`Синхронизация каждые ${intervalMinutes} минут настроена.`);
}

export function stopSyncInterval() {
  if (syncIntervalId) {
    clearInterval(syncIntervalId);
    syncIntervalId = null;
    displayInfo('Интервал синхронизации остановлен.');
  }
}

export async function synchronizeFiles() {
  const currentFtpConnection = await import('./ftp.js').then((module) => module.getCurrentFtpConnection());
  if (!currentFtpConnection) {
    displayInfo('Нет активного FTP-подключения для синхронизации.');
    return;
  }

  const client = await safelyConnectToFtp(currentFtpConnection);
  if (!client) {
    displayInfo('Не удалось подключиться к FTP.');
    return;
  }

  try {
    const tasks = [];
    conflicts = []; // Очистить конфликты перед новой синхронизацией
    for (const file of filesForSync) {
      const remotePath = path.join(currentFtpConnection.remoteDir, file.name);
      const localStats = await files.stat(file.path).catch(() => null);
      try {
        const remoteStats = await client.size(remotePath);

        // Локальный файл новее
        if (localStats && localStats.mtimeMs > remoteStats) {
          tasks.push({ type: 'upload', localPath: file.path, remotePath });
        }
        // Удалённый файл новее
        else if (localStats && localStats.mtimeMs < remoteStats) {
          tasks.push({ type: 'download', localPath: file.path, remotePath });
        }
        // Конфликт (метки времени равны)
        else if (localStats && localStats.mtimeMs === remoteStats) {
          addConflict(file, remotePath);
        }
      } catch {
        tasks.push({ type: 'upload', localPath: file.path, remotePath });
      }
    }
    await processSyncTasks(client, tasks);
    // Проверка на наличие незакрытых конфликтов
    if (conflicts.length > 0) {
      displayInfo('Остались нерешённые конфликты синхронизации.');
    } else {
      displayInfo('Синхронизация завершена!');
    }
  } catch (err) {
    console.error('Ошибка синхронизации файлов:', err);
    displayInfo('Ошибка синхронизации.');
  } finally {
    await safelyCloseFtp(client);
  }
}

export function addConflict(file, remotePath) {
  conflicts.push({ file, remotePath });
  displayInfo('Найден конфликт при синхронизации: ' + file.name);
}

// Параллельная обработка задач синхронизации
export async function processSyncTasks(client, tasks, maxConcurrency = 5) {
  const queue = tasks.slice();

  // Массив промисов для обработки задач
  const workers = Array.from({ length: maxConcurrency }, () =>
    (async function worker() {
      while (queue.length > 0) {
        const task = queue.shift(); // Извлекаем задачу из очереди
        try {
          if (task.type === 'upload') {
            await client.uploadFrom(task.localPath, task.remotePath);
            displayInfo(`Файл "${path.basename(task.localPath)}" успешно загружен.`);
          } else if (task.type === 'download') {
            await client.downloadTo(task.localPath, task.remotePath);
            displayInfo(`Файл "${path.basename(task.remotePath)}" успешно загружен локально.`);
          }
        } catch (err) {
          failedTasks.push({ task, error: err }); // Добавляем задачу в список неудач
          console.error(`Ошибка во время обработки задачи:`, task, err);
        }
      }
    })()
  );

  // Ждем завершения всех потоков
  await Promise.all(workers);

  // Логирование завершенных/неудачных задач
  if (failedTasks.length > 0) {
    displayInfo(`Некоторые задачи синхронизации не выполнены: ${failedTasks.length}`);
    console.error('Неудачные задачи:', failedTasks);
  }
}

export async function uploadFile(client, localPath, remotePath) {
  try {
    await client.uploadFrom(localPath, remotePath);
    displayInfo(`Файл "${path.basename(localPath)}" успешно загружен на сервер.`);
  } catch (err) {
    console.error(`Ошибка загрузки файла "${localPath}":`, err);
    displayInfo(`Ошибка загрузки файла "${path.basename(localPath)}".`);
  }
}

export async function downloadFile(client, remotePath, localPath) {
  try {
    await client.downloadTo(localPath, remotePath);
    displayInfo(`Файл "${path.basename(remotePath)}" успешно загружен с сервера.`);
  } catch (err) {
    console.error(`Ошибка загрузки файла "${remotePath}":`, err);
    displayInfo(`Ошибка загрузки файла "${path.basename(remotePath)}".`);
  }
}

let fileChangeTimeouts = new Map();

export function startFileWatcher() {
  syncOptions = import('./settings.js').then((module) => module.getSyncOptions());
  if (!syncOptions.syncOnChanges) return;

  const currentDir = files.getCurrentDir();
  fileWatcher = fs.watch(currentDir, { recursive: true }, async (eventType, filename) => {
    const filePath = path.join(currentDir, filename);
    const isSynced = filesForSync.some((file) => file.path === filePath);

    if (!isSynced) return;

    if (fileChangeTimeouts.has(filePath)) {
      clearTimeout(fileChangeTimeouts.get(filePath));
    }

    fileChangeTimeouts.set(
      filePath,
      setTimeout(async () => {
        fileChangeTimeouts.delete(filePath);
        const currentFtpConnection = await import('./ftp.js').then((module) => module.getCurrentFtpConnection());
        const client = await safelyConnectToFtp(currentFtpConnection);

        if (client) {
          try {
            const remotePath = path.join(currentFtpConnection.remoteDir, filename);
            if (eventType === 'rename') {
              try {
                await fs.stat(filePath);
                await uploadFile(client, filePath, remotePath);
              } catch {
                await client.remove(remotePath);
              }
            } else if (eventType === 'change') {
              await uploadFile(client, filePath, remotePath);
            }
            displayInfo(`Файл "${filename}" успешно синхронизирован.`);
          } catch (err) {
            console.error(`Ошибка синхронизации файла "${filename}":`, err);
          } finally {
            await safelyCloseFtp(client);
          }
        }
      }, 300)
    );
  });
  displayInfo('Файловое наблюдение включено.');
}

export function cleanUpSync() {
  stopSyncInterval();
  if (fileWatcher) {
    fileWatcher.close();
    fileWatcher = null;
  }
}

export async function toggleSync() {
  const currentFtpConnection = await import('./ftp.js').then((module) => module.getCurrentFtpConnection());
  if (!currentFtpConnection) {
    return displayInfo('Выберите подключение FTP для начала синхронизации.');
  }
  displayInfo('Ручная синхронизация начата...');
  await synchronizeFiles();
}


Файл ui.js:

import * as files from './files.js';
const path = require('path');

let infoDisplay = null;

let filePanelWidth;
let trashPanelWidth;
let folderContentPanelWidth;

export function initUI() {
  infoDisplay = document.getElementById('infoDisplay');
  if (!infoDisplay) {
    console.error('Элемент infoDisplay не найден.');
    return;
  }
}

export function displayInfo(message) {
  infoDisplay.textContent = message;
  setTimeout(() => {
    infoDisplay.textContent = '';
  }, 5000);
}

// Установка начальных размеров панелей
export function setPanelWidths({ filePanelWidth, folderContentPanelWidth, trashPanelWidth }) {
  const filePanel = document.getElementById('filePanel');
  const folderContentPanel = document.getElementById('folderContentPanel');
  const trashPanel = document.getElementById('trashPanel');

  if (filePanel && folderContentPanel && trashPanel) {
    filePanel.style.width = `${filePanelWidth || 300}px`;
    folderContentPanel.style.width = `${folderContentPanelWidth || 300}px`;
    trashPanel.style.width = `${trashPanelWidth || 300}px`;
  }
}

// Обновление ширины третьей панели
export function updateThirdPanelWidth() {
  const filePanel = document.getElementById('filePanel');
  const folderContentPanel = document.getElementById('folderContentPanel');
  const trashPanel = document.getElementById('trashPanel');
  const thirdPanel = document.getElementById('thirdPanel');

  if (!filePanel || !folderContentPanel || !trashPanel || !thirdPanel) {
    console.error('Не найден один из элементов панелей.');
    return;
  }

  const filePanelWidth = filePanel.classList.contains('hidden') ? 0 : filePanel.offsetWidth;
  const folderContentPanelWidth = folderContentPanel.classList.contains('hidden') ? 0 : folderContentPanel.offsetWidth;
  const trashPanelWidth = trashPanel.classList.contains('hidden') ? 0 : trashPanel.offsetWidth;

  const remainingWidth = `calc(100% - ${filePanelWidth + folderContentPanelWidth + trashPanelWidth + 20}px)`;
  thirdPanel.style.width = remainingWidth;
}

// Инициализация ресайзеров
export function initResizers(saveSettings) {
  const gutter1 = document.getElementById('resizer1');
  const gutter2 = document.getElementById('resizer2');
  const gutter3 = document.getElementById('resizer3');
  const filePanel = document.getElementById('filePanel');
  const folderContentPanel = document.getElementById('folderContentPanel');
  const trashPanel = document.getElementById('trashPanel');

  resizer(gutter1, filePanel, 100, 900, saveSettings);
  resizer(gutter2, folderContentPanel, 100, 900, saveSettings);
  resizer(gutter3, trashPanel, 100, 900, saveSettings);
}

// Функция для ресайза панелей
function resizer(gutter, panel, minWidth, maxWidth, saveSettings) {
  if (!gutter || !panel) {
    console.error('Элемент gutter или panel не найден');
    return;
  }

  let animationFrame;
  let prevX = 0;

  function mouseDownHandler(e) {
    e.preventDefault();
    prevX = e.clientX;

    function mouseMoveHandler(event) {
      const dx = event.clientX - prevX;
      const currentWidth = panel.getBoundingClientRect().width;
      const newWidth = Math.max(minWidth, Math.min(currentWidth + dx, maxWidth));
      prevX = event.clientX;

      if (animationFrame) cancelAnimationFrame(animationFrame);
      animationFrame = requestAnimationFrame(() => {
        panel.style.width = `${newWidth}px`;

        // Обновляем глобальные данные
        if (panel.id === 'filePanel') {
          filePanelWidth = newWidth;
        }
        if (panel.id === 'folderContentPanel') {
          folderContentPanelWidth = newWidth;
        }
        if (panel.id === 'trashPanel') {
          trashPanelWidth = newWidth;
        }

        updateThirdPanelWidth();
      });
    }

    function mouseUpHandler() {
      document.removeEventListener('mousemove', mouseMoveHandler);
      document.removeEventListener('mouseup', mouseUpHandler);

      if (animationFrame) cancelAnimationFrame(animationFrame);
      saveSettings(); // Сохраняем настройки только один раз при завершении
    }

    document.addEventListener('mousemove', mouseMoveHandler);
    document.addEventListener('mouseup', mouseUpHandler);
  }

  gutter.addEventListener('mousedown', mouseDownHandler);
}

// Обновление интерфейса
export async function refreshUI() {
  try {
    await loadFolders();
    const currentFolder = document.getElementById('folderContentPanel').getAttribute('data-current-folder');
    if (currentFolder) {
      const folderPath = files.getCurrentDir() + '/' + currentFolder;
      try {
        await loadFiles(folderPath);
      } catch (err) {
        console.warn(`Ошибка доступа к папке ${currentFolder}:`, err);
        document.getElementById('folderContentPanel').classList.add('hidden');
        document.getElementById('folderContentPanel').removeAttribute('data-current-folder');
      }
    }
    await loadTrash();
    displayInfo('Интерфейс обновлён.');
  } catch (err) {
    console.error('Ошибка обновления интерфейса:', err);
    displayInfo('Ошибка загрузки интерфейса.');
  }
}

// Загрузка папок
async function loadFolders() {
  const foldersList = document.getElementById('foldersList');
  foldersList.innerHTML = '';

  try {
    const items = await files.readDir(files.getCurrentDir(), { withFileTypes: true });
    const folders = items.filter((item) => item.isDirectory());

    if (folders.length === 0) {
      foldersList.innerHTML = '<p>Папок не найдено</p>';
    } else {
      folders.forEach((folder) => {
        const folderDiv = document.createElement('div');
        folderDiv.textContent = folder.name;
        folderDiv.classList.add('draggable');
        folderDiv.setAttribute('data-type', 'folder');
        folderDiv.addEventListener('click', () => openFolder(folder.name));
        folderDiv.addEventListener('contextmenu', (e) => showContextMenu(e, folder.name, 'folder'));
        foldersList.appendChild(folderDiv);
      });
    }

    displayInfo(`Загружены папки: ${folders.length}`);
    initializeContextMenu();
  } catch (err) {
    console.error('Ошибка загрузки папок:', err);
    foldersList.innerHTML = '<p>Папки не найдены или текущая директория недоступна.</p>';
  }
}

// Загрузка файлов
async function loadFiles(folderPath) {
  const filesList = document.getElementById('filesList');
  filesList.innerHTML = '';

  try {
    const items = await files.readDir(folderPath, { withFileTypes: true });
    const filesData = items.filter((item) => item.isFile());

    if (filesData.length === 0) {
      filesList.innerHTML = '<p>Папка пуста</p>';
    } else {
      filesData.forEach((file) => {
        const fileDiv = document.createElement('div');
        fileDiv.textContent = file.name;
        fileDiv.classList.add('draggable');
        fileDiv.setAttribute('data-type', 'file');
        fileDiv.addEventListener('click', () => displayInfo(`Выбран файл: ${file.name}`));
        fileDiv.addEventListener('contextmenu', (e) => showContextMenu(e, file.name, 'file'));
        filesList.appendChild(fileDiv);
      });
    }
    displayInfo(`Загружено файлов: ${filesData.length}`);
    initializeContextMenu();
  } catch (err) {
    console.error('Ошибка загрузки файлов:', err);
    displayInfo('Не удалось загрузить файлы.');
  }
}

// Загрузка корзины
async function loadTrash() {
  const trashList = document.getElementById('trashList');
  trashList.innerHTML = '';

  try {
    const trash = await files.loadTrash();

    if (trash.length === 0) {
      trashList.innerHTML = `<p>Корзина пуста</p>`;
    } else {
      trash.forEach((item) => {
        const trashDiv = document.createElement('div');
        trashDiv.textContent = item.name;
        trashDiv.classList.add('draggable');
        trashDiv.setAttribute('data-type', 'trash');
        trashList.appendChild(trashDiv);
      });
    }
    displayInfo(`Корзина загружена: ${trash.length}`);
    initializeContextMenu();
  } catch (err) {
    console.error('Ошибка загрузки корзины:', err);
    displayInfo('Ошибка загрузки корзины.');
  }
}

// Инициализация контекстного меню
function initializeContextMenu() {
  const draggableElements = document.querySelectorAll('.draggable');
  draggableElements.forEach((element) => {
    element.addEventListener('contextmenu', (event) => {
      const itemType = element.getAttribute('data-type');
      const itemName = element.textContent;
      showContextMenu(event, itemName, itemType);
    });
  });

  document.addEventListener('contextmenu', (event) => {
    event.preventDefault();
  });
}

// Показ контекстного меню
function showContextMenu(e, itemName, itemType) {
  e.preventDefault();
  const menu = document.getElementById('contextMenu');
  menu.style.left = `${e.pageX}px`;
  menu.style.top = `${e.pageY}px`;
  menu.classList.add('visible');

  const deleteOption = menu.querySelector('#deleteOption');
  const restoreOption = menu.querySelector('#restoreOption');
  const renameOption = menu.querySelector('#renameOption');
  const syncOption = menu.querySelector('#syncOption');

  if (itemType === 'file' || itemType === 'folder') {
    deleteOption.style.display = 'block';
    restoreOption.style.display = 'none';
    renameOption.style.display = 'block';
    syncOption.style.display = 'block';

    const folderContentPanel = document.getElementById('folderContentPanel');
    const itemPath =
      itemType === 'file'
        ? files.getCurrentDir() + '/' + (folderContentPanel.dataset.currentFolder || '') + '/' + itemName
        : files.getCurrentDir() + '/' + itemName;

    deleteOption.onclick = () => files.moveToTrash(itemPath, itemType);
    renameOption.onclick = () => files.renameItem(itemPath, itemType);
    syncOption.onclick = () => {
      const targetElement = e.target.closest('.draggable');
      files.toggleSyncStatus(itemPath, itemType, targetElement);
      updateSyncIndicators(files.getFilesForSync());
    };
  } else if (itemType === 'trash') {
    deleteOption.style.display = 'block';
    restoreOption.style.display = 'block';
    renameOption.style.display = 'none';
    syncOption.style.display = 'none';

    deleteOption.onclick = () => files.deleteFromTrash(itemName);
    restoreOption.onclick = () => files.restoreFromTrash(itemName);
  }

  const hideContextMenu = () => {
    menu.classList.remove('visible');
    document.removeEventListener('click', hideContextMenu);
  };

  document.addEventListener('click', hideContextMenu);
}

// Открытие папки
async function openFolder(folderName) {
  const folderContentPanel = document.getElementById('folderContentPanel');
  const resizer2 = document.getElementById('resizer2');
  const folderPath = files.getCurrentDir() + '/' + folderName;

  if (folderContentPanel.getAttribute('data-current-folder') === folderName) {
    folderContentPanel.classList.add('hidden');
    resizer2.classList.add('hidden');
    folderContentPanel.removeAttribute('data-current-folder');
    return;
  }

  folderContentPanel.classList.remove('hidden');
  resizer2.classList.remove('hidden');

  try {
    await loadFiles(folderPath);
    folderContentPanel.setAttribute('data-current-folder', folderName);
  } catch (err) {
    console.error(`Ошибка загрузки содержимого папки "${folderName}":`, err);
    displayInfo(`Не удалось открыть папку "${folderName}".`);
  }
}

// Обновление индикаторов синхронизации
export function updateSyncIndicators(filesForSync, conflicts = []) {
  const items = document.querySelectorAll('.draggable');

  items.forEach((item) => {
    // Получаем путь элемента
    const itemPath = item.getAttribute('data-path') || path.join(files.getCurrentDir(), item.textContent);

    // Проверяем, есть ли элемент в конфликтах
    const isConflict = conflicts.some((conflict) => conflict.file.path === itemPath);
    if (isConflict) {
      item.classList.remove('synced', 'unsynced');
      item.classList.add('conflict');
      return; // Если конфликт, больше ничего не обновляем для этого элемента
    }

    // Проверяем, синхронизируется ли текущий файл/папка (или его родитель)
    const isSynced = filesForSync.some((file) => itemPath.startsWith(file.path));

    // Обновляем класс в зависимости от статуса
    if (isSynced) {
      item.classList.remove('unsynced', 'conflict');
      item.classList.add('synced');
    } else {
      item.classList.remove('synced', 'conflict');
      item.classList.add('unsynced');
    }
  });
}

// Открытие настроек синхронизации
export function openSyncSettings() {
  document.getElementById('syncSettingsDialog').classList.remove('hidden');
}

// Закрытие настроек синхронизации
export function closeSyncSettings() {
  document.getElementById('syncSettingsDialog').classList.add('hidden');
}

// Показать/скрыть панель для файлов (filePanel)
export function toggleFilePanel() {
  const filePanel = document.getElementById('filePanel');
  const folderContentPanel = document.getElementById('folderContentPanel');
  const resizer1 = document.getElementById('resizer1');
  const resizer2 = document.getElementById('resizer2');

  if (filePanel.classList.contains('hidden')) {
    filePanel.classList.remove('hidden');
    resizer1.classList.remove('hidden');

    if (folderContentPanel.dataset.openWhenHidden === 'true') {
      folderContentPanel.classList.remove('hidden');
      resizer2.classList.remove('hidden');
    }
  } else {
    filePanel.classList.add('hidden');
    resizer1.classList.add('hidden');

    if (!folderContentPanel.classList.contains('hidden')) {
      folderContentPanel.dataset.openWhenHidden = 'true';
      folderContentPanel.classList.add('hidden');
      resizer2.classList.add('hidden');
    } else {
      folderContentPanel.dataset.openWhenHidden = 'false';
    }
  }
  updateThirdPanelWidth();
}

// Показать/скрыть панель для корзины (trashPanel)
export async function toggleTrashPanel() {
  const trashPanel = document.getElementById('trashPanel');
  const resizer3 = document.getElementById('resizer3');

  if (trashPanel.classList.contains('hidden')) {
    trashPanel.classList.remove('hidden');
    resizer3.classList.remove('hidden');
    await loadTrash();
  } else {
    trashPanel.classList.add('hidden');
    resizer3.classList.add('hidden');
  }
  updateThirdPanelWidth();
}

Проанализируй код приложение. Давай улучшим модульность, оптимизируем код, оптимизируем слушателей событий. Сделай всё, чтобы код можно было легко модишицировать, дополнять функционалом.