1287 lines
55 KiB
HTML
1287 lines
55 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Органайзер АйТи-Депо</title>
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/jstree.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/themes/default/style.min.css">
|
||
<style>
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
height: 100vh;
|
||
background-color: #f4f4f4;
|
||
transition: background-color 0.3s;
|
||
}
|
||
body.dark-theme {
|
||
background-color: #222;
|
||
color: #fff;
|
||
}
|
||
#tree-container, #installs-container, #notes-panel {
|
||
overflow: auto;
|
||
}
|
||
#tree-container {
|
||
width: 250px;
|
||
min-width: 200px;
|
||
border-right: 1px solid #ccc;
|
||
padding: 10px;
|
||
background-color: #fff;
|
||
transition: width 0.3s;
|
||
}
|
||
#tree-container.dark-theme {
|
||
border-right-color: #444;
|
||
background-color: #333;
|
||
}
|
||
#installs-container {
|
||
flex-grow: 1;
|
||
padding: 10px;
|
||
background-color: #fff;
|
||
}
|
||
#installs-container.dark-theme {
|
||
background-color: #333;
|
||
}
|
||
#notes-panel {
|
||
width: 300px;
|
||
min-width: 200px;
|
||
border-left: 1px solid #ccc;
|
||
padding: 10px;
|
||
background-color: #fff;
|
||
}
|
||
#notes-panel.dark-theme {
|
||
border-left-color: #444;
|
||
background-color: #333;
|
||
}
|
||
#splitter {
|
||
width: 5px;
|
||
background-color: #ccc;
|
||
cursor: col-resize;
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
#splitter.dark-theme {
|
||
background-color: #444;
|
||
}
|
||
.header-logo {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
.header-logo img {
|
||
height: 40px;
|
||
margin-right: 10px;
|
||
}
|
||
.theme-toggle, #add-record-button {
|
||
padding: 5px 10px;
|
||
cursor: pointer;
|
||
}
|
||
.theme-toggle.dark-theme, #add-record-button.dark-theme {
|
||
background-color: #555;
|
||
color: #fff;
|
||
border: 1px solid #777;
|
||
}
|
||
.export-import-container {
|
||
margin-bottom: 10px;
|
||
}
|
||
#search-container {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
}
|
||
#search-container.dark-theme input, #search-container.dark-theme select {
|
||
background-color: #444;
|
||
color: #fff;
|
||
border: 1px solid #666;
|
||
}
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-weight: bold;
|
||
padding: 5px;
|
||
background-color: #e0e0e0;
|
||
cursor: pointer;
|
||
}
|
||
.header.dark-theme {
|
||
background-color: #444;
|
||
}
|
||
.header div {
|
||
flex: 1;
|
||
text-align: center;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.sort-arrow {
|
||
margin-left: 5px;
|
||
}
|
||
.install-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 5px 8px; /* Увеличим внутренний отступ для большей "воздушности" */
|
||
border-bottom: 1px solid #ddd;
|
||
cursor: pointer;
|
||
align-items: center;
|
||
}
|
||
.install-item.dark-theme {
|
||
border-bottom-color: #555;
|
||
background-color: #444;
|
||
}
|
||
.install-item:hover {
|
||
background-color: #f0f0f0;
|
||
}
|
||
.install-item.dark-theme:hover {
|
||
background-color: #555;
|
||
}
|
||
.install-item.selected {
|
||
background-color: #d0d0d0;
|
||
}
|
||
.install-item.dark-theme.selected {
|
||
background-color: #666;
|
||
}
|
||
.computer-name, .connection-id, .install-time, .actions {
|
||
flex: 1;
|
||
text-align: center;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.protocol-icon img {
|
||
height: 32px;
|
||
width: 32px;
|
||
margin-right: 8px; /* Увеличим отступ для большей читаемости */
|
||
vertical-align: middle;
|
||
}
|
||
.connection-link {
|
||
color: #007bff;
|
||
text-decoration: none;
|
||
flex-grow: 1;
|
||
margin-right: 8px;
|
||
}
|
||
.connection-link.dark-theme {
|
||
color: #66b0ff;
|
||
}
|
||
.action-buttons button {
|
||
margin: 0 2px;
|
||
padding: 2px 5px;
|
||
display: flex;
|
||
align-items: center;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
}
|
||
.action-buttons.dark-theme button {
|
||
background-color: #555;
|
||
color: #fff;
|
||
border: 1px solid #777;
|
||
}
|
||
/* Стили для иконок */
|
||
.custom-icon {
|
||
height: 32px;
|
||
width: 32px;
|
||
background-size: contain;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
background-color: transparent;
|
||
border: none;
|
||
padding: 0;
|
||
margin: 0 4px; /* Добавляем отступы вокруг иконок */
|
||
cursor: pointer;
|
||
transition: background-color 0.2s, transform 0.2s;
|
||
}
|
||
.custom-icon:hover {
|
||
background-color: #e0e0e0;
|
||
transform: scale(1.1);
|
||
}
|
||
.custom-icon.dark-theme:hover {
|
||
background-color: #444;
|
||
}
|
||
/* Иконки для светлой темы */
|
||
.edit-icon { background-image: url('/icons/edit.png'); }
|
||
.copy-icon { background-image: url('/icons/copy.png'); }
|
||
/* Иконки для темной темы */
|
||
.edit-icon.dark-theme { background-image: url('/icons/edit-dark.png'); }
|
||
.copy-icon.dark-theme { background-image: url('/icons/copy-dark.png'); }
|
||
/* Иконки для папок (меньший размер) */
|
||
.folder-edit-icon, .folder-delete-icon {
|
||
height: 16px;
|
||
width: 16px;
|
||
background-size: contain;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
background-color: transparent;
|
||
border: none;
|
||
padding: 0;
|
||
margin: 0 2px; /* Небольшой отступ для папочных иконок */
|
||
cursor: pointer;
|
||
transition: background-color 0.2s, transform 0.2s;
|
||
}
|
||
.folder-edit-icon:hover, .folder-delete-icon:hover {
|
||
background-color: #e0e0e0;
|
||
transform: scale(1.1);
|
||
}
|
||
.folder-edit-icon.dark-theme:hover, .folder-delete-icon.dark-theme:hover {
|
||
background-color: #444;
|
||
}
|
||
.folder-edit-icon { background-image: url('/icons/edit.png'); }
|
||
.folder-delete-icon { background-image: url('/icons/delete.png'); }
|
||
.folder-edit-icon.dark-theme { background-image: url('/icons/edit-dark.png'); }
|
||
.folder-delete-icon.dark-theme { background-image: url('/icons/delete-dark.png'); }
|
||
/* Стили для кнопок удаления и заметок */
|
||
.action-button {
|
||
margin: 0 4px; /* Увеличим расстояние между кнопками */
|
||
padding: 5px 8px;
|
||
font-size: 12px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
background-color: #f9f9f9;
|
||
color: #333;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
||
}
|
||
.action-button:hover {
|
||
background-color: #e0e0e0;
|
||
}
|
||
.action-button.dark-theme {
|
||
background-color: #555;
|
||
color: #fff;
|
||
border-color: #777;
|
||
}
|
||
.action-button.dark-theme:hover {
|
||
background-color: #666;
|
||
}
|
||
#notes-content {
|
||
padding: 10px;
|
||
min-height: 200px;
|
||
border: 1px solid #ccc;
|
||
}
|
||
#notes-content.dark-theme {
|
||
border-color: #444;
|
||
background-color: #444;
|
||
}
|
||
#modal-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
}
|
||
#add-modal, #note-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: #fff;
|
||
padding: 20px;
|
||
border-radius: 5px;
|
||
z-index: 1001;
|
||
width: 400px;
|
||
}
|
||
#add-modal.dark-theme, #note-modal.dark-theme {
|
||
background: #333;
|
||
color: #fff;
|
||
}
|
||
#add-modal input, #add-modal select, #add-modal textarea,
|
||
#note-modal textarea {
|
||
width: 100%;
|
||
margin-bottom: 10px;
|
||
padding: 5px;
|
||
}
|
||
#add-modal.dark-theme input, #add-modal.dark-theme select, #add-modal.dark-theme textarea,
|
||
#note-modal.dark-theme textarea {
|
||
background-color: #444;
|
||
color: #fff;
|
||
border: 1px solid #666;
|
||
}
|
||
.markdown-buttons, .image-upload {
|
||
margin-bottom: 10px;
|
||
}
|
||
.time-hint {
|
||
font-size: 0.8em;
|
||
color: #666;
|
||
}
|
||
.time-hint.dark-theme {
|
||
color: #bbb;
|
||
}
|
||
.status-online {
|
||
background-color: green;
|
||
width: 10px;
|
||
height: 10px;
|
||
display: inline-block;
|
||
border-radius: 50%;
|
||
vertical-align: middle;
|
||
margin-left: 5px;
|
||
}
|
||
.status-offline {
|
||
background-color: red;
|
||
width: 10px;
|
||
height: 10px;
|
||
display: inline-block;
|
||
border-radius: 50%;
|
||
vertical-align: middle;
|
||
margin-left: 5px;
|
||
}
|
||
.highlight {
|
||
background-color: #e0e0e0;
|
||
}
|
||
.highlight.dark-theme {
|
||
background-color: #555;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="tree-container">
|
||
<h2>Папки</h2>
|
||
<div id="folder-tree"></div>
|
||
<button onclick="addFolder()">Добавить папку</button>
|
||
</div>
|
||
<div id="splitter"></div>
|
||
<div id="installs-container">
|
||
<h1 class="header-logo">
|
||
<img src="/icons/it-depo-logo.png" alt="АйТи-Депо логотип">
|
||
Органайзер АйТи-Депо
|
||
<button class="theme-toggle" onclick="toggleTheme()">Темная тема</button>
|
||
</h1>
|
||
<div class="export-import-container">
|
||
<button onclick="exportCSV()">Экспорт CSV</button>
|
||
<form id="import-form" enctype="multipart/form-data">
|
||
<input type="file" id="import-file" accept=".csv" onchange="importCSV(this.files[0])">
|
||
<label for="import-file" id="import-label">Импорт CSV</label>
|
||
</form>
|
||
</div>
|
||
<div id="search-container">
|
||
<input type="text" id="search-input" placeholder="Поиск по ID или имени">
|
||
<select id="folder-select">
|
||
<option value="">Все папки</option>
|
||
</select>
|
||
<button id="add-record-button" onclick="openAddModal()">Добавить запись</button>
|
||
</div>
|
||
<div class="header">
|
||
<div onclick="sortInstalls('computer_name')">Имя компьютера<span class="sort-arrow"></span></div>
|
||
<div onclick="sortInstalls('rust_id')">ID подключения<span class="sort-arrow"></span></div>
|
||
<div onclick="sortInstalls('install_time')">Время установки<span class="sort-arrow"></span></div>
|
||
<div>Действия</div>
|
||
</div>
|
||
<div id="installs-list"></div>
|
||
</div>
|
||
<div id="notes-panel">
|
||
<h2>Заметка</h2>
|
||
<div id="notes-content"></div>
|
||
</div>
|
||
|
||
<div id="modal-overlay"></div>
|
||
<div id="add-modal">
|
||
<h3>Добавить запись</h3>
|
||
<input type="text" id="add-rustId" placeholder="ID подключения" required>
|
||
<input type="text" id="add-computerName" placeholder="Имя компьютера" required>
|
||
<input type="text" id="add-installTime" placeholder="Время (опционально, формат: ГГГГ-ММ-ДД ЧЧ:ММ:СС)">
|
||
<select id="add-protocol">
|
||
<option value="rustdesk" selected>RustDesk</option>
|
||
<option value="anydesk">AnyDesk</option>
|
||
<option value="ammyy">Ammyy Admin</option>
|
||
<option value="teamviewer">TeamViewer</option>
|
||
<option value="vnc">VNC</option>
|
||
<option value="rdp">RDP</option>
|
||
</select>
|
||
<textarea id="add-note" placeholder="Заметка (Markdown поддерживается)"></textarea>
|
||
<button onclick="addInstall()">Добавить</button>
|
||
<button onclick="closeAddModal()">Закрыть</button>
|
||
<div class="time-hint">Пример: 2025-03-05 14:30:00</div>
|
||
</div>
|
||
<div id="note-modal">
|
||
<h3>Редактировать заметку</h3>
|
||
<div class="markdown-buttons">
|
||
<button onclick="formatBold()">Жирный</button>
|
||
<button onclick="formatItalic()">Курсив</button>
|
||
<button onclick="formatLink()">Ссылка</button>
|
||
</div>
|
||
<div class="image-upload">
|
||
<input type="file" id="image-upload-input" accept="image/*" onchange="uploadImage()">
|
||
<button onclick="document.getElementById('image-upload-input').click()">Добавить изображение</button>
|
||
</div>
|
||
<textarea id="note-textarea" placeholder="Введите заметку (Markdown поддерживается)"></textarea>
|
||
<button onclick="saveNote()">Сохранить</button>
|
||
<button onclick="closeNoteModal()">Закрыть</button>
|
||
</div>
|
||
|
||
<script>
|
||
const API_URL = "/api";
|
||
let selectedFolderId = null;
|
||
let allInstalls = [];
|
||
let allFolders = [];
|
||
let selectedInstallId = null;
|
||
let sortField = null;
|
||
let sortDirection = 'asc';
|
||
let ws = null;
|
||
|
||
const protocolIcons = {
|
||
'rustdesk': '<img src="/icons/rustdesk.png" class="protocol-icon">',
|
||
'anydesk': '<img src="/icons/anydesk.png" class="protocol-icon">',
|
||
'ammyy': '<img src="/icons/ammyy.png" class="protocol-icon">',
|
||
'teamviewer': '<img src="/icons/teamviewer.png" class="protocol-icon">',
|
||
'vnc': '<img src="/icons/vnc.png" class="protocol-icon">',
|
||
'rdp': '<img src="/icons/rdp.png" class="protocol-icon">'
|
||
};
|
||
|
||
const protocolLinks = {
|
||
'rustdesk': 'rustdesk://',
|
||
'anydesk': 'anydesk://',
|
||
'ammyy': 'ammyy://',
|
||
'teamviewer': 'teamviewer://remote-control/',
|
||
'vnc': 'vnc://',
|
||
'rdp': 'rdp://'
|
||
};
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const savedWidth = localStorage.getItem('treeContainerWidth');
|
||
if (savedWidth) {
|
||
document.getElementById('tree-container').style.width = savedWidth + 'px';
|
||
}
|
||
|
||
const savedTheme = localStorage.getItem('theme');
|
||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
const defaultTheme = savedTheme || (prefersDark ? 'dark' : 'light');
|
||
if (defaultTheme === 'dark') {
|
||
document.body.classList.add('dark-theme');
|
||
document.getElementById('installs-container').classList.add('dark-theme');
|
||
document.getElementById('tree-container').classList.add('dark-theme');
|
||
document.getElementById('notes-panel').classList.add('dark-theme');
|
||
document.getElementById('add-modal').classList.add('dark-theme');
|
||
document.getElementById('note-modal').classList.add('dark-theme');
|
||
document.getElementById('search-container').classList.add('dark-theme');
|
||
document.getElementById('import-label').classList.add('dark-theme');
|
||
document.getElementById('splitter').classList.add('dark-theme');
|
||
document.querySelector('.theme-toggle').classList.add('dark-theme');
|
||
document.querySelector('#add-record-button').classList.add('dark-theme');
|
||
document.querySelector('.header').classList.add('dark-theme');
|
||
document.querySelectorAll('.install-item').forEach(item => item.classList.add('dark-theme'));
|
||
document.querySelectorAll('.action-button').forEach(button => button.classList.add('dark-theme'));
|
||
$('#folder-tree').addClass('jstree-default-dark');
|
||
document.querySelector('.theme-toggle').textContent = 'Светлая тема';
|
||
} else {
|
||
document.querySelector('.theme-toggle').textContent = 'Темная тема';
|
||
}
|
||
localStorage.setItem('theme', defaultTheme);
|
||
|
||
const splitter = document.getElementById('splitter');
|
||
const treeContainer = document.getElementById('tree-container');
|
||
let isResizing = false;
|
||
|
||
splitter.addEventListener('mousedown', (e) => {
|
||
isResizing = true;
|
||
document.body.style.cursor = 'col-resize';
|
||
e.preventDefault();
|
||
});
|
||
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (!isResizing) return;
|
||
|
||
const containerRect = document.body.getBoundingClientRect();
|
||
let newWidth = e.clientX - containerRect.left;
|
||
const minWidth = 200;
|
||
const maxWidth = containerRect.width * 0.5;
|
||
|
||
if (newWidth < minWidth) newWidth = minWidth;
|
||
if (newWidth > maxWidth) newWidth = maxWidth;
|
||
|
||
treeContainer.style.width = `${newWidth}px`;
|
||
});
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
isResizing = false;
|
||
document.body.style.cursor = 'default';
|
||
const width = treeContainer.offsetWidth;
|
||
localStorage.setItem('treeContainerWidth', width);
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (!selectedFolderId || !allInstalls.length) return;
|
||
|
||
const items = $('#installs-list .install-item');
|
||
let currentIndex = items.index($('.install-item.selected'));
|
||
|
||
if (e.key === 'ArrowUp' && currentIndex > 0) {
|
||
e.preventDefault();
|
||
currentIndex--;
|
||
} else if (e.key === 'ArrowDown' && currentIndex < items.length - 1) {
|
||
e.preventDefault();
|
||
currentIndex++;
|
||
} else {
|
||
return;
|
||
}
|
||
|
||
selectedInstallId = $(items[currentIndex]).data('id');
|
||
$('.install-item').removeClass('selected');
|
||
$(items[currentIndex]).addClass('selected');
|
||
updateNotesPanel();
|
||
$(items[currentIndex])[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
});
|
||
|
||
connectWebSocket();
|
||
});
|
||
|
||
function toggleTheme() {
|
||
const body = document.body;
|
||
const installsContainer = document.getElementById('installs-container');
|
||
const treeContainer = document.getElementById('tree-container');
|
||
const notesPanel = document.getElementById('notes-panel');
|
||
const addModal = document.getElementById('add-modal');
|
||
const noteModal = document.getElementById('note-modal');
|
||
const searchContainer = document.getElementById('search-container');
|
||
const importLabel = document.getElementById('import-label');
|
||
const splitter = document.getElementById('splitter');
|
||
const themeToggle = document.querySelector('.theme-toggle');
|
||
const addRecordButton = document.getElementById('add-record-button');
|
||
const header = document.querySelector('.header');
|
||
const installItems = document.querySelectorAll('.install-item');
|
||
const actionButtons = document.querySelectorAll('.action-button');
|
||
const folderTree = $('#folder-tree');
|
||
|
||
if (body.classList.contains('dark-theme')) {
|
||
body.classList.remove('dark-theme');
|
||
installsContainer.classList.remove('dark-theme');
|
||
treeContainer.classList.remove('dark-theme');
|
||
notesPanel.classList.remove('dark-theme');
|
||
addModal.classList.remove('dark-theme');
|
||
noteModal.classList.remove('dark-theme');
|
||
searchContainer.classList.remove('dark-theme');
|
||
importLabel.classList.remove('dark-theme');
|
||
splitter.classList.remove('dark-theme');
|
||
themeToggle.classList.remove('dark-theme');
|
||
addRecordButton.classList.remove('dark-theme');
|
||
header.classList.remove('dark-theme');
|
||
installItems.forEach(item => item.classList.remove('dark-theme'));
|
||
actionButtons.forEach(button => button.classList.remove('dark-theme'));
|
||
folderTree.removeClass('jstree-default-dark');
|
||
themeToggle.textContent = 'Темная тема';
|
||
localStorage.setItem('theme', 'light');
|
||
} else {
|
||
body.classList.add('dark-theme');
|
||
installsContainer.classList.add('dark-theme');
|
||
treeContainer.classList.add('dark-theme');
|
||
notesPanel.classList.add('dark-theme');
|
||
addModal.classList.add('dark-theme');
|
||
noteModal.classList.add('dark-theme');
|
||
searchContainer.classList.add('dark-theme');
|
||
importLabel.classList.add('dark-theme');
|
||
splitter.classList.add('dark-theme');
|
||
themeToggle.classList.add('dark-theme');
|
||
addRecordButton.classList.add('dark-theme');
|
||
header.classList.add('dark-theme');
|
||
installItems.forEach(item => item.classList.add('dark-theme'));
|
||
actionButtons.forEach(button => button.classList.add('dark-theme'));
|
||
folderTree.addClass('jstree-default-dark');
|
||
themeToggle.textContent = 'Светлая тема';
|
||
localStorage.setItem('theme', 'dark');
|
||
}
|
||
}
|
||
|
||
$(document).ready(function () {
|
||
loadFolders();
|
||
loadInstalls(null);
|
||
|
||
$('#folder-tree').jstree({
|
||
'core': {
|
||
'data': function (node, cb) {
|
||
$.getJSON(`${API_URL}/folders`, function (data) {
|
||
allFolders = data;
|
||
const sortedFolders = [...data].sort((a, b) => a.name.localeCompare(b.name));
|
||
const treeData = [
|
||
{ id: "root", text: "Корень", parent: "#" }
|
||
].concat(sortedFolders.map(folder => ({
|
||
id: folder.id,
|
||
text: `${folder.name} <span class="folder-actions">` +
|
||
`<button class="folder-edit-icon" title="Редактировать папку" onclick="editFolder(${folder.id}, '${folder.name}')"></button>` +
|
||
`<button class="folder-delete-icon" title="Удалить папку" onclick="deleteFolder(${folder.id})"></button></span>`,
|
||
parent: folder.parent_id ? folder.parent_id : "root"
|
||
})));
|
||
cb(treeData);
|
||
|
||
const unsortedFolder = sortedFolders.find(f => f.name === 'Несортированные');
|
||
if (unsortedFolder) {
|
||
setTimeout(() => {
|
||
$('#folder-tree').jstree('select_node', unsortedFolder.id);
|
||
loadInstalls(unsortedFolder.id);
|
||
updateFolderActions();
|
||
}, 100);
|
||
}
|
||
}).fail(function(jqxhr, textStatus, error) {
|
||
console.error("Error loading folders:", textStatus, error);
|
||
});
|
||
},
|
||
'check_callback': true,
|
||
'themes': {
|
||
'variant': 'default-dark'
|
||
}
|
||
},
|
||
'plugins': ['dnd', 'html_data'],
|
||
'dnd': {
|
||
'is_draggable': true
|
||
}
|
||
}).on('select_node.jstree', function (e, data) {
|
||
selectedFolderId = data.node.id === "root" ? null : data.node.id;
|
||
loadInstalls(selectedFolderId);
|
||
updateFolderActions();
|
||
}).on('move_node.jstree', function (e, data) {
|
||
const movedNode = data.node;
|
||
const newParentId = movedNode.parent === '#' ? null : movedNode.parent;
|
||
saveFolderPosition(movedNode.id, newParentId);
|
||
}).on('dragover.jstree', function (e) {
|
||
e.preventDefault();
|
||
const node = $(e.target).closest('.jstree-node');
|
||
if (node.length) {
|
||
node.addClass('highlight').addClass(document.body.classList.contains('dark-theme') ? 'dark-theme' : '');
|
||
}
|
||
}).on('dragenter.jstree', function (e) {
|
||
e.preventDefault();
|
||
const node = $(e.target).closest('.jstree-node');
|
||
if (node.length) {
|
||
node.addClass('highlight').addClass(document.body.classList.contains('dark-theme') ? 'dark-theme' : '');
|
||
}
|
||
}).on('dragleave.jstree', function (e) {
|
||
const node = $(e.target).closest('.jstree-node');
|
||
if (node.length) {
|
||
node.removeClass('highlight');
|
||
}
|
||
}).on('drop.jstree', function (e) {
|
||
const node = $(e.target).closest('.jstree-node');
|
||
if (node.length) {
|
||
node.removeClass('highlight');
|
||
}
|
||
});
|
||
|
||
$('#search-input').on('input', function () {
|
||
performSearch();
|
||
});
|
||
|
||
$('#folder-select').on('change', function () {
|
||
performSearch();
|
||
});
|
||
|
||
$('#installs-list').on('dragstart', '.install-item', function (e) {
|
||
e.originalEvent.dataTransfer.setData('text/plain', $(this).data('id'));
|
||
});
|
||
|
||
$('#folder-tree').on('dragover', function (e) {
|
||
e.preventDefault();
|
||
}).on('drop', function (e) {
|
||
e.preventDefault();
|
||
const installId = e.originalEvent.dataTransfer.getData('text');
|
||
let targetFolderId = $(e.target).closest('.jstree-node').attr('id');
|
||
if (installId && targetFolderId) {
|
||
targetFolderId = targetFolderId === "root" ? null : targetFolderId;
|
||
moveInstallToFolder(installId, targetFolderId);
|
||
}
|
||
});
|
||
|
||
$('#installs-list').on('click', '.note-button', function () {
|
||
const installId = $(this).closest('.install-item').data('id');
|
||
const install = allInstalls.find(i => i.id === installId);
|
||
openNoteModal(installId, install?.note || '');
|
||
});
|
||
});
|
||
|
||
function connectWebSocket() {
|
||
if (ws) {
|
||
ws.close();
|
||
}
|
||
ws = new WebSocket(`wss://${window.location.host}/ws`);
|
||
ws.onmessage = function(event) {
|
||
const data = JSON.parse(event.data);
|
||
if (data.type === "update") {
|
||
const updatedInstall = data.data;
|
||
const index = allInstalls.findIndex(i => i.id === updatedInstall.id);
|
||
if (index !== -1) {
|
||
allInstalls[index] = updatedInstall;
|
||
} else {
|
||
allInstalls.push(updatedInstall);
|
||
alert(`Новое подключение добавлено: ${updatedInstall.computer_name} (ID: ${updatedInstall.rust_id})`);
|
||
}
|
||
displayInstalls(allInstalls.filter(i => !selectedFolderId || i.folder_id == selectedFolderId));
|
||
}
|
||
};
|
||
ws.onerror = function(error) {
|
||
console.error('WebSocket error:', error);
|
||
};
|
||
ws.onclose = function() {
|
||
console.log('WebSocket disconnected, retrying...');
|
||
let retryDelay = 1000;
|
||
const maxDelay = 30000;
|
||
const maxAttempts = 10;
|
||
let attempts = 0;
|
||
|
||
function reconnect() {
|
||
if (attempts < maxAttempts) {
|
||
setTimeout(() => {
|
||
connectWebSocket();
|
||
retryDelay = Math.min(retryDelay * 2, maxDelay);
|
||
attempts++;
|
||
}, retryDelay);
|
||
} else {
|
||
console.error('Max reconnect attempts reached');
|
||
}
|
||
}
|
||
reconnect();
|
||
};
|
||
}
|
||
|
||
function loadFolders() {
|
||
$.getJSON(`${API_URL}/folders`, function (data) {
|
||
allFolders = [...data].sort((a, b) => a.name.localeCompare(b.name));
|
||
updateFolderSelect();
|
||
}).fail(function(jqxhr, textStatus, error) {
|
||
console.error("Error loading folders:", textStatus, error);
|
||
});
|
||
}
|
||
|
||
function loadInstalls(folderId) {
|
||
$.getJSON(`${API_URL}/installs`, function (data) {
|
||
allInstalls = data;
|
||
let filtered = folderId ? data.filter(i => i.folder_id == folderId && i.folder_id !== null) : data;
|
||
displayInstalls(filtered);
|
||
updateNotesPanel();
|
||
if (selectedInstallId) {
|
||
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
|
||
}
|
||
}).fail(function(jqxhr, textStatus, error) {
|
||
console.error("Error loading installs:", textStatus, error);
|
||
$('#installs-list').html('<p>Ошибка загрузки данных. Проверьте консоль.</p>');
|
||
});
|
||
selectedFolderId = folderId;
|
||
}
|
||
|
||
function performSearch() {
|
||
const query = $('#search-input').val().toLowerCase().trim();
|
||
const folderId = $('#folder-select').val() || '';
|
||
|
||
let filteredInstalls = [...allInstalls];
|
||
|
||
if (folderId) {
|
||
filteredInstalls = filteredInstalls.filter(i =>
|
||
i.folder_id == folderId &&
|
||
allFolders.some(f => f.id === i.folder_id)
|
||
);
|
||
}
|
||
|
||
if (query) {
|
||
filteredInstalls = filteredInstalls.filter(i =>
|
||
i.rust_id.toLowerCase().trim().includes(query) ||
|
||
i.computer_name.toLowerCase().trim().includes(query)
|
||
);
|
||
}
|
||
|
||
sortInstalls(null, filteredInstalls);
|
||
}
|
||
|
||
function displayInstalls(installs) {
|
||
const isDarkTheme = document.body.classList.contains('dark-theme');
|
||
if (installs.length === 0) {
|
||
$('#installs-list').html('<p>Нет результатов</p>');
|
||
} else {
|
||
$('#installs-list').html(installs.map(item => {
|
||
const lastSeen = new Date(item.last_seen || '1970-01-01');
|
||
const isOnline = (new Date() - lastSeen) < 60000;
|
||
const statusClass = isOnline ? 'status-online' : 'status-offline';
|
||
return `
|
||
<div class="install-item ${isDarkTheme ? 'dark-theme' : ''}" data-id="${item.id}" draggable="true">
|
||
<div class="computer-name">${item.computer_name} <button class="custom-icon edit-icon" title="Редактировать имя компьютера" onclick="editField(${item.id}, 'computer_name', '${item.computer_name}')"></button></div>
|
||
<div class="connection-id"><span class="protocol-icon">${protocolIcons[item.protocol]}</span><a href="${protocolLinks[item.protocol]}${item.rust_id}" class="connection-link ${isDarkTheme ? 'dark-theme' : ''}" target="_blank">${item.rust_id}</a><span class="${statusClass}"></span> <button class="custom-icon copy-icon" title="Скопировать ID подключения" onclick="copyToClipboard('${item.rust_id}')"></button> <button class="custom-icon edit-icon" title="Редактировать ID подключения" onclick="editField(${item.id}, 'rust_id', '${item.rust_id}')"></button></div>
|
||
<div class="install-time">${item.install_time} <button class="custom-icon edit-icon" title="Редактировать время установки" onclick="editField(${item.id}, 'install_time', '${item.install_time}')"></button></div>
|
||
<div class="actions"><span class="action-buttons"><button class="action-button delete-button ${isDarkTheme ? 'dark-theme' : ''}" title="Удалить запись" onclick="deleteInstall(${item.id})">Удалить</button> <button class="action-button note-button ${isDarkTheme ? 'dark-theme' : ''}" title="Добавить или редактировать заметку">Заметка</button></span></div>
|
||
</div>
|
||
`;
|
||
}).join(''));
|
||
}
|
||
updateSortArrows();
|
||
if (selectedInstallId) {
|
||
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
|
||
}
|
||
}
|
||
|
||
function selectInstall(installId, rustId, protocol) {
|
||
selectedInstallId = installId;
|
||
$('.install-item').removeClass('selected');
|
||
$(`#installs-list .install-item[data-id="${installId}"]`).addClass('selected');
|
||
updateNotesPanel();
|
||
const link = protocolLinks[protocol] + rustId;
|
||
window.location.href = link;
|
||
}
|
||
|
||
function sortInstalls(field, filteredInstalls = null) {
|
||
let installs = filteredInstalls || (selectedFolderId
|
||
? allInstalls.filter(i => i.folder_id == selectedFolderId && i.folder_id !== null)
|
||
: allInstalls);
|
||
|
||
if (field) {
|
||
if (sortField === field) {
|
||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
sortField = field;
|
||
sortDirection = 'asc';
|
||
}
|
||
installs.sort((a, b) => {
|
||
let valA = a[field], valB = b[field];
|
||
if (field === 'install_time') {
|
||
valA = new Date(valA);
|
||
valB = new Date(valB);
|
||
}
|
||
return sortDirection === 'asc' ? (valA > valB ? 1 : -1) : (valA < valB ? 1 : -1);
|
||
});
|
||
}
|
||
displayInstalls(installs);
|
||
}
|
||
|
||
function updateSortArrows() {
|
||
$('.sort-arrow').text('');
|
||
if (sortField) {
|
||
$(`.header div:contains("${sortField === 'computer_name' ? 'Имя компьютера' : sortField === 'rust_id' ? 'ID подключения' : 'Время установки'}") .sort-arrow`)
|
||
.text(sortDirection === 'asc' ? '↑' : '↓');
|
||
}
|
||
}
|
||
|
||
function addFolder() {
|
||
const name = prompt("Введите имя папки:");
|
||
if (name) {
|
||
$.ajax({
|
||
url: `${API_URL}/folders`,
|
||
type: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({ name, parent_id: selectedFolderId }),
|
||
success: function () {
|
||
loadFolders();
|
||
$('#folder-tree').jstree(true).refresh();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function editFolder(folderId, currentName) {
|
||
const newName = prompt("Введите новое имя папки:", currentName);
|
||
if (newName) {
|
||
$.ajax({
|
||
url: `${API_URL}/folders/${folderId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({ name: newName }),
|
||
success: function () {
|
||
loadFolders();
|
||
$('#folder-tree').jstree(true).refresh();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function deleteFolder(folderId) {
|
||
if (confirm("Удалить папку? Все записи будут перемещены в корень.")) {
|
||
$.ajax({
|
||
url: `${API_URL}/folders/${folderId}`,
|
||
type: 'DELETE',
|
||
success: function () {
|
||
$.getJSON(`${API_URL}/installs`, function (data) {
|
||
const folderInstalls = data.filter(i => i.folder_id == folderId);
|
||
folderInstalls.forEach(item => {
|
||
moveInstallToFolder(item.id, null);
|
||
});
|
||
});
|
||
loadFolders();
|
||
$('#folder-tree').jstree(true).refresh();
|
||
loadInstalls(selectedFolderId);
|
||
},
|
||
error: function (xhr, status, error) {
|
||
if (xhr.status === 403) {
|
||
alert("Нельзя удалить папку 'Несортированные'.");
|
||
} else {
|
||
console.error("Ошибка удаления:", status, error);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function openAddModal() {
|
||
$('#add-rustId').val('');
|
||
$('#add-computerName').val('');
|
||
$('#add-installTime').val('');
|
||
$('#add-protocol').val('rustdesk');
|
||
$('#add-note').val('');
|
||
$('#add-modal').show();
|
||
$('#modal-overlay').show();
|
||
}
|
||
|
||
function closeAddModal() {
|
||
$('#add-modal').hide();
|
||
$('#modal-overlay').hide();
|
||
}
|
||
|
||
function addInstall() {
|
||
const rustId = $('#add-rustId').val();
|
||
const computerName = $('#add-computerName').val();
|
||
const installTime = $('#add-installTime').val() || '';
|
||
const protocol = $('#add-protocol').val();
|
||
const note = $('#add-note').val() || '';
|
||
|
||
if (installTime && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(installTime)) {
|
||
alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)");
|
||
return;
|
||
}
|
||
|
||
$.ajax({
|
||
url: `${API_URL}/install`,
|
||
type: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({ rust_id: rustId, computer_name: computerName, install_time: installTime, folder_id: selectedFolderId, protocol: protocol, note: note }),
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
closeAddModal();
|
||
},
|
||
error: function (xhr, status, error) {
|
||
if (xhr.status === 400) {
|
||
alert(xhr.responseJSON.detail);
|
||
} else {
|
||
console.error("Ошибка добавления:", status, error);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function editField(installId, field, currentValue) {
|
||
let newValue;
|
||
if (field === 'install_time') {
|
||
newValue = prompt(`Новое ${field === 'computer_name' ? 'Имя компьютера' : field === 'rust_id' ? 'ID подключения' : 'Время установки'} (опционально, формат для времени: ГГГГ-ММ-ДД ЧЧ:ММ:СС):`, currentValue || '');
|
||
if (newValue && field === 'install_time' && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(newValue)) {
|
||
alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)");
|
||
return;
|
||
}
|
||
} else {
|
||
newValue = prompt(`Новое ${field === 'computer_name' ? 'Имя компьютера' : field === 'rust_id' ? 'ID подключения' : 'Время установки'}:`, currentValue || '');
|
||
}
|
||
|
||
if (newValue !== null && newValue !== currentValue) {
|
||
let data = {};
|
||
data[field] = newValue;
|
||
if (field === 'install_time') {
|
||
data['folder_id'] = selectedFolderId;
|
||
}
|
||
const install = allInstalls.find(i => i.id === installId);
|
||
if (install) {
|
||
data['protocol'] = install.protocol;
|
||
data['note'] = install.note;
|
||
}
|
||
$.ajax({
|
||
url: `${API_URL}/install/${installId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify(data),
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
},
|
||
error: function (xhr, status, error) {
|
||
if (xhr.status === 400) {
|
||
alert(xhr.responseJSON.detail);
|
||
} else {
|
||
console.error(`Ошибка редактирования ${field}:`, status, error);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function deleteInstall(id) {
|
||
if (confirm("Удалить запись?")) {
|
||
$.ajax({
|
||
url: `${API_URL}/install/${id}`,
|
||
type: 'DELETE',
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function moveInstallToFolder(installId, folderId) {
|
||
const install = allInstalls.find(i => i.id === parseInt(installId));
|
||
if (!install) {
|
||
console.error('Install not found:', installId);
|
||
return;
|
||
}
|
||
$.ajax({
|
||
url: `${API_URL}/install/${installId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({
|
||
rust_id: install.rust_id,
|
||
computer_name: install.computer_name,
|
||
install_time: install.install_time,
|
||
folder_id: folderId,
|
||
protocol: install.protocol,
|
||
note: install.note
|
||
}),
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
},
|
||
error: function (xhr, status, error) {
|
||
console.error("Ошибка переноса:", status, error);
|
||
if (xhr.status === 404) {
|
||
alert("Запись не найдена");
|
||
} else {
|
||
alert("Ошибка при перемещении записи");
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function exportCSV() {
|
||
const folderId = $('#folder-select').val() || '';
|
||
window.location.href = `${API_URL}/export/csv?folder_id=${folderId}`;
|
||
}
|
||
|
||
function importCSV(file) {
|
||
if (!file) return;
|
||
|
||
const folderId = $('#folder-select').val() || '';
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
if (folderId) {
|
||
formData.append('folder_id', folderId);
|
||
}
|
||
|
||
$.ajax({
|
||
url: `${API_URL}/import/csv`,
|
||
type: 'POST',
|
||
data: formData,
|
||
processData: false,
|
||
contentType: false,
|
||
success: function (response) {
|
||
alert(response.message);
|
||
loadInstalls(selectedFolderId);
|
||
},
|
||
error: function (xhr, status, error) {
|
||
alert(`Ошибка импорта: ${xhr.responseJSON.detail || error}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
function openNoteModal(installId, currentNote) {
|
||
selectedInstallId = installId;
|
||
$('#note-textarea').val(currentNote || '');
|
||
$('#note-modal').show();
|
||
$('#modal-overlay').show();
|
||
document.getElementById('image-upload-input').value = '';
|
||
}
|
||
|
||
function uploadImage() {
|
||
const fileInput = document.getElementById('image-upload-input');
|
||
const file = fileInput.files[0];
|
||
if (!file || !selectedInstallId) {
|
||
alert('Пожалуйста, выберите запись и изображение.');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('install_id', selectedInstallId);
|
||
formData.append('file', file);
|
||
|
||
$.ajax({
|
||
url: `${API_URL}/upload-image`,
|
||
type: 'POST',
|
||
data: formData,
|
||
processData: false,
|
||
contentType: false,
|
||
success: function (response) {
|
||
const imageUrl = response.url;
|
||
const textarea = $('#note-textarea');
|
||
const cursorPos = textarea[0].selectionStart;
|
||
const text = textarea.val();
|
||
const altText = prompt('Введите альтернативный текст для изображения:', file.name);
|
||
const markdownImage = ``;
|
||
textarea.val(text.substring(0, cursorPos) + markdownImage + text.substring(cursorPos));
|
||
textarea[0].selectionStart = cursorPos + markdownImage.length;
|
||
textarea[0].selectionEnd = cursorPos + markdownImage.length;
|
||
},
|
||
error: function (xhr, status, error) {
|
||
console.error('Error uploading image:', status, error);
|
||
alert(`Не удалось загрузить изображение: ${xhr.responseJSON?.detail || 'Неизвестная ошибка'}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
function saveNote() {
|
||
if (!selectedInstallId) {
|
||
alert('Выберите запись для редактирования заметки.');
|
||
return;
|
||
}
|
||
|
||
const note = $('#note-textarea').val() || '';
|
||
const install = allInstalls.find(i => i.id === selectedInstallId);
|
||
if (install) {
|
||
const currentSelectedId = selectedInstallId;
|
||
$.ajax({
|
||
url: `${API_URL}/install/${selectedInstallId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({
|
||
rust_id: install.rust_id,
|
||
computer_name: install.computer_name,
|
||
install_time: install.install_time,
|
||
folder_id: install.folder_id,
|
||
protocol: install.protocol,
|
||
note: note
|
||
}),
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
selectedInstallId = currentSelectedId;
|
||
closeNoteModal();
|
||
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
|
||
updateNotesPanel();
|
||
},
|
||
error: function (xhr, status, error) {
|
||
console.error("Ошибка сохранения заметки:", status, error);
|
||
alert(`Не удалось сохранить заметку: ${xhr.responseJSON?.detail || error}`);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function closeNoteModal() {
|
||
$('#note-modal').hide();
|
||
$('#modal-overlay').hide();
|
||
$('#note-textarea').val('');
|
||
document.getElementById('image-upload-input').value = '';
|
||
if (selectedInstallId) {
|
||
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
|
||
updateNotesPanel();
|
||
}
|
||
}
|
||
|
||
function updateNotesPanel() {
|
||
if (selectedInstallId) {
|
||
const install = allInstalls.find(i => i.id === selectedInstallId);
|
||
if (install && install.note) {
|
||
$('#notes-content').html(marked.parse(install.note));
|
||
} else {
|
||
$('#notes-content').html('<p>Нет заметок</p>');
|
||
}
|
||
} else {
|
||
$('#notes-content').html('<p>Выберите подключение для просмотра заметки</p>');
|
||
}
|
||
}
|
||
|
||
function updateFolderActions() {
|
||
$('.folder-actions').hide();
|
||
if (selectedFolderId) {
|
||
$(`#${selectedFolderId} .folder-actions`).show();
|
||
} else {
|
||
$('#root .folder-actions').show();
|
||
}
|
||
}
|
||
|
||
function updateFolderSelect() {
|
||
const $folderSelect = $('#folder-select');
|
||
$folderSelect.empty().append('<option value="">Все папки</option>');
|
||
allFolders.forEach(folder => {
|
||
$folderSelect.append(`<option value="${folder.id}">${folder.name}</option>`);
|
||
});
|
||
}
|
||
|
||
$('#installs-list').on('click', '.install-item', function () {
|
||
selectedInstallId = $(this).data('id');
|
||
$('.install-item').removeClass('selected');
|
||
$(this).addClass('selected');
|
||
updateNotesPanel();
|
||
});
|
||
|
||
function formatBold() {
|
||
const textarea = $('#note-textarea');
|
||
const start = textarea[0].selectionStart;
|
||
const end = textarea[0].selectionEnd;
|
||
const text = textarea.val();
|
||
const selected = text.substring(start, end);
|
||
const newText = `**${selected}**`;
|
||
textarea.val(text.substring(0, start) + newText + text.substring(end));
|
||
textarea[0].selectionStart = start;
|
||
textarea[0].selectionEnd = start + newText.length - 4;
|
||
}
|
||
|
||
function formatItalic() {
|
||
const textarea = $('#note-textarea');
|
||
const start = textarea[0].selectionStart;
|
||
const end = textarea[0].selectionEnd;
|
||
const text = textarea.val();
|
||
const selected = text.substring(start, end);
|
||
const newText = `*${selected}*`;
|
||
textarea.val(text.substring(0, start) + newText + text.substring(end));
|
||
textarea[0].selectionStart = start;
|
||
textarea[0].selectionEnd = start + newText.length - 2;
|
||
}
|
||
|
||
function formatLink() {
|
||
const textarea = $('#note-textarea');
|
||
const start = textarea[0].selectionStart;
|
||
const end = textarea[0].selectionEnd;
|
||
const text = textarea.val();
|
||
const selected = text.substring(start, end) || 'текст';
|
||
const url = prompt('Введите URL ссылки:', 'https://example.com');
|
||
if (url) {
|
||
const newText = `[${selected}](${url})`;
|
||
textarea.val(text.substring(0, start) + newText + text.substring(end));
|
||
textarea[0].selectionStart = start;
|
||
textarea[0].selectionEnd = start + newText.length - (selected ? 0 : '[текст]'.length);
|
||
}
|
||
}
|
||
|
||
function copyToClipboard(text) {
|
||
navigator.clipboard.writeText(text).catch(err => {
|
||
console.error('Ошибка копирования: ', err);
|
||
});
|
||
}
|
||
|
||
function saveFolderPosition(folderId, newParentId) {
|
||
const folder = allFolders.find(f => f.id == folderId);
|
||
if (!folder) {
|
||
console.error(`Folder with id ${folderId} not found`);
|
||
return;
|
||
}
|
||
|
||
$.ajax({
|
||
url: `${API_URL}/folders/${folderId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({ name: folder.name, parent_id: newParentId }),
|
||
success: function () {
|
||
loadFolders();
|
||
$('#folder-tree').jstree(true).refresh();
|
||
},
|
||
error: function (xhr, status, error) {
|
||
console.error("Error saving folder position:", status, error);
|
||
console.error("Response:", xhr.responseText);
|
||
alert("Не удалось сохранить новое положение папки. Проверьте консоль.");
|
||
$('#folder-tree').jstree(true).refresh();
|
||
}
|
||
});
|
||
}
|
||
|
||
function startPeriodicSync() {
|
||
setInterval(() => {
|
||
$.getJSON(`${API_URL}/installs`, function (data) {
|
||
allInstalls = data;
|
||
displayInstalls(allInstalls.filter(i => !selectedFolderId || i.folder_id == selectedFolderId));
|
||
}).fail(function(jqxhr, textStatus, error) {
|
||
console.error("Error syncing installs:", textStatus, error);
|
||
});
|
||
}, 60000);
|
||
}
|
||
|
||
startPeriodicSync();
|
||
</script>
|
||
</body>
|
||
</html> |