- Добавлены функции для переключения между темной и светлой темами с использованием localStorage. - Обновлены стили для поддержки темной темы, включая цвета фона, текста и иконок. - Добавлены кнопки для переключения темы на страницах входа, профиля, заметок и настроек. - Оптимизирован код для предотвращения мерцания темы при загрузке страницы.
525 lines
17 KiB
JavaScript
525 lines
17 KiB
JavaScript
// Логика переключения темы
|
||
function initThemeToggle() {
|
||
const themeToggleBtn = document.getElementById("theme-toggle-btn");
|
||
|
||
if (!themeToggleBtn) return;
|
||
|
||
// Загружаем сохраненную тему или используем системную
|
||
const savedTheme = localStorage.getItem("theme");
|
||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||
? "dark"
|
||
: "light";
|
||
const currentTheme = savedTheme || systemTheme;
|
||
|
||
// Применяем тему
|
||
applyTheme(currentTheme);
|
||
|
||
// Обработчик клика на переключатель
|
||
themeToggleBtn.addEventListener("click", () => {
|
||
const currentTheme = document.documentElement.getAttribute("data-theme");
|
||
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
||
applyTheme(newTheme);
|
||
localStorage.setItem("theme", newTheme);
|
||
});
|
||
|
||
// Слушаем изменения системной темы
|
||
window
|
||
.matchMedia("(prefers-color-scheme: dark)")
|
||
.addEventListener("change", (e) => {
|
||
if (!localStorage.getItem("theme")) {
|
||
applyTheme(e.matches ? "dark" : "light");
|
||
}
|
||
});
|
||
}
|
||
|
||
function applyTheme(theme) {
|
||
document.documentElement.setAttribute("data-theme", theme);
|
||
|
||
// Обновляем meta теги для PWA
|
||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||
if (themeColorMeta) {
|
||
themeColorMeta.setAttribute(
|
||
"content",
|
||
theme === "dark" ? "#1a1a1a" : "#007bff"
|
||
);
|
||
}
|
||
|
||
// Обновляем иконку переключателя
|
||
const themeToggleBtn = document.getElementById("theme-toggle-btn");
|
||
if (themeToggleBtn) {
|
||
const icon = themeToggleBtn.querySelector(".iconify");
|
||
if (icon) {
|
||
icon.setAttribute(
|
||
"data-icon",
|
||
theme === "dark" ? "mdi:weather-sunny" : "mdi:weather-night"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Инициализируем переключатель темы при загрузке страницы
|
||
document.addEventListener("DOMContentLoaded", initThemeToggle);
|
||
|
||
// Переменные для пагинации логов
|
||
let logsOffset = 0;
|
||
const logsLimit = 50;
|
||
let hasMoreLogs = true;
|
||
|
||
// DOM элементы для внешнего вида
|
||
const settingsAccentColorInput = document.getElementById(
|
||
"settings-accentColor"
|
||
);
|
||
const updateAppearanceBtn = document.getElementById("updateAppearanceBtn");
|
||
|
||
// Проверка аутентификации
|
||
async function checkAuthentication() {
|
||
try {
|
||
const response = await fetch("/api/auth/status");
|
||
if (!response.ok) {
|
||
localStorage.removeItem("isAuthenticated");
|
||
localStorage.removeItem("username");
|
||
window.location.href = "/";
|
||
return false;
|
||
}
|
||
const authData = await response.json();
|
||
if (!authData.authenticated) {
|
||
localStorage.removeItem("isAuthenticated");
|
||
localStorage.removeItem("username");
|
||
window.location.href = "/";
|
||
return false;
|
||
}
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Ошибка проверки аутентификации:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Загрузка информации о пользователе для применения accent color и заполнения формы
|
||
async function loadUserInfo() {
|
||
try {
|
||
const response = await fetch("/api/user");
|
||
if (response.ok) {
|
||
const user = await response.json();
|
||
const accentColor = user.accent_color || "#007bff";
|
||
|
||
// Применяем цветовой акцент
|
||
if (
|
||
getComputedStyle(document.documentElement)
|
||
.getPropertyValue("--accent-color")
|
||
.trim() !== accentColor
|
||
) {
|
||
document.documentElement.style.setProperty(
|
||
"--accent-color",
|
||
accentColor
|
||
);
|
||
}
|
||
|
||
// Заполняем поле цветового акцента в настройках
|
||
if (settingsAccentColorInput) {
|
||
settingsAccentColorInput.value = accentColor;
|
||
updateColorPickerSelection(accentColor);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка загрузки информации о пользователе:", error);
|
||
}
|
||
}
|
||
|
||
// Функция для обновления выбора цвета в цветовой палитре
|
||
function updateColorPickerSelection(selectedColor) {
|
||
const colorOptions = document.querySelectorAll(
|
||
"#appearance-tab .color-option"
|
||
);
|
||
colorOptions.forEach((option) => {
|
||
option.classList.remove("selected");
|
||
if (option.dataset.color === selectedColor) {
|
||
option.classList.add("selected");
|
||
}
|
||
});
|
||
}
|
||
|
||
// Функция для показа сообщений
|
||
function showSettingsMessage(message, type = "success") {
|
||
const container = document.getElementById("settings-message-container");
|
||
if (container) {
|
||
container.textContent = message;
|
||
container.className = `message-container ${type}`;
|
||
container.style.display = "block";
|
||
|
||
setTimeout(() => {
|
||
container.style.display = "none";
|
||
}, 5000);
|
||
}
|
||
}
|
||
|
||
// Переключение табов
|
||
function initTabs() {
|
||
const tabs = document.querySelectorAll(".settings-tab");
|
||
const contents = document.querySelectorAll(".tab-content");
|
||
|
||
tabs.forEach((tab) => {
|
||
tab.addEventListener("click", () => {
|
||
const tabName = tab.dataset.tab;
|
||
|
||
// Убираем активный класс со всех табов и контентов
|
||
tabs.forEach((t) => t.classList.remove("active"));
|
||
contents.forEach((c) => c.classList.remove("active"));
|
||
|
||
// Добавляем активный класс к выбранному табу и контенту
|
||
tab.classList.add("active");
|
||
document.getElementById(`${tabName}-tab`).classList.add("active");
|
||
|
||
// Загружаем данные для таба
|
||
if (tabName === "archive") {
|
||
loadArchivedNotes();
|
||
} else if (tabName === "logs") {
|
||
loadLogs(true);
|
||
} else if (tabName === "appearance") {
|
||
// Данные внешнего вида уже загружены в loadUserInfo()
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Загрузка архивных заметок
|
||
async function loadArchivedNotes() {
|
||
const container = document.getElementById("archived-notes-container");
|
||
container.innerHTML =
|
||
'<p style="text-align: center; color: #999;">Загрузка...</p>';
|
||
|
||
try {
|
||
const response = await fetch("/api/notes/archived");
|
||
if (!response.ok) {
|
||
throw new Error("Ошибка загрузки архивных заметок");
|
||
}
|
||
|
||
const notes = await response.json();
|
||
|
||
if (notes.length === 0) {
|
||
container.innerHTML =
|
||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = "";
|
||
|
||
notes.forEach((note) => {
|
||
const noteDiv = document.createElement("div");
|
||
noteDiv.className = "archived-note-item";
|
||
noteDiv.dataset.noteId = note.id;
|
||
|
||
// Форматируем дату
|
||
const created = new Date(note.created_at.replace(" ", "T") + "Z");
|
||
const dateStr = new Intl.DateTimeFormat("ru-RU", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
}).format(created);
|
||
|
||
// Преобразуем markdown в HTML для предпросмотра
|
||
const htmlContent = marked.parse(note.content);
|
||
const preview =
|
||
htmlContent.substring(0, 200) + (htmlContent.length > 200 ? "..." : "");
|
||
|
||
// Изображения
|
||
let imagesHtml = "";
|
||
if (note.images && note.images.length > 0) {
|
||
imagesHtml = `<div class="archived-note-images">${note.images.length} изображений</div>`;
|
||
}
|
||
|
||
noteDiv.innerHTML = `
|
||
<div class="archived-note-header">
|
||
<span class="archived-note-date">${dateStr}</span>
|
||
<div class="archived-note-actions">
|
||
<button class="btn-restore" data-id="${note.id}" title="Восстановить">
|
||
<span class="iconify" data-icon="mdi:restore"></span> Восстановить
|
||
</button>
|
||
<button class="btn-delete-permanent" data-id="${note.id}" title="Удалить навсегда">
|
||
<span class="iconify" data-icon="mdi:delete-forever"></span> Удалить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="archived-note-content">${preview}</div>
|
||
${imagesHtml}
|
||
`;
|
||
|
||
container.appendChild(noteDiv);
|
||
});
|
||
|
||
// Добавляем обработчики событий
|
||
addArchivedNotesEventListeners();
|
||
} catch (error) {
|
||
console.error("Ошибка загрузки архивных заметок:", error);
|
||
container.innerHTML =
|
||
'<p style="text-align: center; color: #dc3545;">Ошибка загрузки архивных заметок</p>';
|
||
}
|
||
}
|
||
|
||
// Добавление обработчиков для архивных заметок
|
||
function addArchivedNotesEventListeners() {
|
||
// Восстановление
|
||
document.querySelectorAll(".btn-restore").forEach((btn) => {
|
||
btn.addEventListener("click", async (e) => {
|
||
const noteId = e.target.closest("button").dataset.id;
|
||
if (confirm("Восстановить эту заметку из архива?")) {
|
||
try {
|
||
const response = await fetch(`/api/notes/${noteId}/unarchive`, {
|
||
method: "PUT",
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error("Ошибка восстановления заметки");
|
||
}
|
||
|
||
// Удаляем элемент из списка
|
||
document.querySelector(`[data-note-id="${noteId}"]`)?.remove();
|
||
|
||
// Проверяем, остались ли заметки
|
||
const container = document.getElementById("archived-notes-container");
|
||
if (container.children.length === 0) {
|
||
container.innerHTML =
|
||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||
}
|
||
|
||
alert("Заметка восстановлена!");
|
||
} catch (error) {
|
||
console.error("Ошибка:", error);
|
||
alert("Ошибка восстановления заметки");
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// Окончательное удаление
|
||
document.querySelectorAll(".btn-delete-permanent").forEach((btn) => {
|
||
btn.addEventListener("click", async (e) => {
|
||
const noteId = e.target.closest("button").dataset.id;
|
||
if (
|
||
confirm("Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!")
|
||
) {
|
||
try {
|
||
const response = await fetch(`/api/notes/archived/${noteId}`, {
|
||
method: "DELETE",
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error("Ошибка удаления заметки");
|
||
}
|
||
|
||
// Удаляем элемент из списка
|
||
document.querySelector(`[data-note-id="${noteId}"]`)?.remove();
|
||
|
||
// Проверяем, остались ли заметки
|
||
const container = document.getElementById("archived-notes-container");
|
||
if (container.children.length === 0) {
|
||
container.innerHTML =
|
||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||
}
|
||
|
||
alert("Заметка удалена окончательно");
|
||
} catch (error) {
|
||
console.error("Ошибка:", error);
|
||
alert("Ошибка удаления заметки");
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Загрузка логов
|
||
async function loadLogs(reset = false) {
|
||
if (reset) {
|
||
logsOffset = 0;
|
||
hasMoreLogs = true;
|
||
}
|
||
|
||
const tbody = document.getElementById("logsTableBody");
|
||
const loadMoreContainer = document.getElementById("logsLoadMore");
|
||
const filterValue = document.getElementById("logTypeFilter").value;
|
||
|
||
if (reset) {
|
||
tbody.innerHTML =
|
||
'<tr><td colspan="4" style="text-align: center;">Загрузка...</td></tr>';
|
||
}
|
||
|
||
try {
|
||
let url = `/api/logs?limit=${logsLimit}&offset=${logsOffset}`;
|
||
if (filterValue) {
|
||
url += `&action_type=${filterValue}`;
|
||
}
|
||
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
throw new Error("Ошибка загрузки логов");
|
||
}
|
||
|
||
const logs = await response.json();
|
||
|
||
if (reset) {
|
||
tbody.innerHTML = "";
|
||
}
|
||
|
||
if (logs.length === 0 && logsOffset === 0) {
|
||
tbody.innerHTML =
|
||
'<tr><td colspan="4" style="text-align: center; color: #999;">Логов пока нет</td></tr>';
|
||
loadMoreContainer.style.display = "none";
|
||
return;
|
||
}
|
||
|
||
if (logs.length < logsLimit) {
|
||
hasMoreLogs = false;
|
||
}
|
||
|
||
logs.forEach((log) => {
|
||
const row = document.createElement("tr");
|
||
|
||
// Форматируем дату
|
||
const created = new Date(log.created_at.replace(" ", "T") + "Z");
|
||
const dateStr = new Intl.DateTimeFormat("ru-RU", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
}).format(created);
|
||
|
||
// Форматируем тип действия
|
||
const actionTypes = {
|
||
login: "Вход",
|
||
logout: "Выход",
|
||
register: "Регистрация",
|
||
note_create: "Создание заметки",
|
||
note_update: "Редактирование",
|
||
note_delete: "Удаление",
|
||
note_pin: "Закрепление",
|
||
note_archive: "Архивирование",
|
||
note_unarchive: "Восстановление",
|
||
note_delete_permanent: "Окончательное удаление",
|
||
profile_update: "Обновление профиля",
|
||
};
|
||
|
||
const actionText = actionTypes[log.action_type] || log.action_type;
|
||
|
||
row.innerHTML = `
|
||
<td>${dateStr}</td>
|
||
<td><span class="log-action-badge log-action-${
|
||
log.action_type
|
||
}">${actionText}</span></td>
|
||
<td>${log.details || "-"}</td>
|
||
<td>${log.ip_address || "-"}</td>
|
||
`;
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
logsOffset += logs.length;
|
||
|
||
if (hasMoreLogs && logs.length > 0) {
|
||
loadMoreContainer.style.display = "block";
|
||
} else {
|
||
loadMoreContainer.style.display = "none";
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка загрузки логов:", error);
|
||
if (reset) {
|
||
tbody.innerHTML =
|
||
'<tr><td colspan="4" style="text-align: center; color: #dc3545;">Ошибка загрузки логов</td></tr>';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Инициализация при загрузке страницы
|
||
document.addEventListener("DOMContentLoaded", async function () {
|
||
// Проверяем аутентификацию
|
||
const isAuth = await checkAuthentication();
|
||
if (!isAuth) return;
|
||
|
||
// Загружаем информацию о пользователе
|
||
await loadUserInfo();
|
||
|
||
// Инициализируем табы
|
||
initTabs();
|
||
|
||
// Обработчик фильтра логов
|
||
document.getElementById("logTypeFilter").addEventListener("change", () => {
|
||
loadLogs(true);
|
||
});
|
||
|
||
// Обработчик кнопки обновления логов
|
||
document.getElementById("refreshLogs").addEventListener("click", () => {
|
||
loadLogs(true);
|
||
});
|
||
|
||
// Обработчик кнопки "Загрузить еще"
|
||
document.getElementById("loadMoreLogsBtn").addEventListener("click", () => {
|
||
loadLogs(false);
|
||
});
|
||
|
||
// Обработчик кнопки сохранения внешнего вида
|
||
if (updateAppearanceBtn) {
|
||
updateAppearanceBtn.addEventListener("click", async function () {
|
||
const accentColor = settingsAccentColorInput.value;
|
||
|
||
try {
|
||
const response = await fetch("/api/user/profile", {
|
||
method: "PUT",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
accent_color: accentColor,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const error = await response.json();
|
||
throw new Error(error.error || "Ошибка обновления цветового акцента");
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
// Применяем новый цветовой акцент
|
||
document.documentElement.style.setProperty(
|
||
"--accent-color",
|
||
accentColor
|
||
);
|
||
|
||
showSettingsMessage(
|
||
result.message || "Цветовой акцент успешно обновлен",
|
||
"success"
|
||
);
|
||
} catch (error) {
|
||
console.error("Ошибка обновления цветового акцента:", error);
|
||
showSettingsMessage(error.message, "error");
|
||
}
|
||
});
|
||
}
|
||
|
||
// Обработчики для цветовой палитры
|
||
function setupAppearanceColorPicker() {
|
||
const colorOptions = document.querySelectorAll(
|
||
"#appearance-tab .color-option"
|
||
);
|
||
colorOptions.forEach((option) => {
|
||
option.addEventListener("click", function () {
|
||
const selectedColor = this.dataset.color;
|
||
settingsAccentColorInput.value = selectedColor;
|
||
updateColorPickerSelection(selectedColor);
|
||
});
|
||
});
|
||
|
||
// Обработчик для input color
|
||
if (settingsAccentColorInput) {
|
||
settingsAccentColorInput.addEventListener("input", function () {
|
||
updateColorPickerSelection(this.value);
|
||
});
|
||
}
|
||
}
|
||
|
||
setupAppearanceColorPicker();
|
||
});
|