NoteJS/public/settings.js
Fovway 8fd529302f Реализовано переключение темной и светлой темы в интерфейсе
- Добавлены функции для переключения между темной и светлой темами с использованием localStorage.
- Обновлены стили для поддержки темной темы, включая цвета фона, текста и иконок.
- Добавлены кнопки для переключения темы на страницах входа, профиля, заметок и настроек.
- Оптимизирован код для предотвращения мерцания темы при загрузке страницы.
2025-10-25 17:14:04 +07:00

525 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Логика переключения темы
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();
});