Добавлены новые настройки пользователя: возможность отображения даты редактирования и использования цветных иконок. Обновлены соответствующие компоненты и стили для поддержки новых функций. Реализована логика добавления колонок в базу данных при отсутствии. Обновлены API и интерфейсы для работы с новыми параметрами.

This commit is contained in:
Fovway 2025-11-03 01:00:52 +07:00
parent 143338bc2b
commit 8c3db305ca
8 changed files with 343 additions and 126 deletions

View File

@ -464,6 +464,42 @@ function runMigrations() {
});
}
// Проверяем существование колонки show_edit_date
const hasShowEditDate = columns.some((col) => col.name === "show_edit_date");
if (!hasShowEditDate) {
db.run(
"ALTER TABLE users ADD COLUMN show_edit_date INTEGER DEFAULT 1",
(err) => {
if (err) {
console.error(
"Ошибка добавления колонки show_edit_date:",
err.message
);
} else {
console.log("Колонка show_edit_date добавлена в таблицу users");
}
}
);
}
// Проверяем существование колонки colored_icons
const hasColoredIcons = columns.some((col) => col.name === "colored_icons");
if (!hasColoredIcons) {
db.run(
"ALTER TABLE users ADD COLUMN colored_icons INTEGER DEFAULT 1",
(err) => {
if (err) {
console.error(
"Ошибка добавления колонки colored_icons:",
err.message
);
} else {
console.log("Колонка colored_icons добавлена в таблицу users");
}
}
);
}
// Проверяем существование колонки ai_enabled
const hasAiEnabled = columns.some((col) => col.name === "ai_enabled");
if (!hasAiEnabled) {
@ -733,7 +769,7 @@ app.get("/api/user", requireApiAuth, (req, res) => {
}
const sql =
"SELECT username, email, avatar, accent_color FROM users WHERE id = ?";
"SELECT username, email, avatar, accent_color, show_edit_date, colored_icons FROM users WHERE id = ?";
db.get(sql, [req.session.userId], (err, user) => {
if (err) {
console.error("Ошибка получения данных пользователя:", err.message);
@ -1528,7 +1564,7 @@ app.get("/profile", requireAuth, (req, res) => {
// API для обновления профиля
app.put("/api/user/profile", requireApiAuth, async (req, res) => {
const { username, email, currentPassword, newPassword, accent_color } =
const { username, email, currentPassword, newPassword, accent_color, show_edit_date, colored_icons } =
req.body;
const userId = req.session.userId;
@ -1594,6 +1630,16 @@ app.put("/api/user/profile", requireApiAuth, async (req, res) => {
params.push(accent_color || "#007bff");
}
if (show_edit_date !== undefined) {
updateFields.push("show_edit_date = ?");
params.push(show_edit_date ? 1 : 0);
}
if (colored_icons !== undefined) {
updateFields.push("colored_icons = ?");
params.push(colored_icons ? 1 : 0);
}
if (newPassword) {
const hashedPassword = await bcrypt.hash(newPassword, 10);
updateFields.push("password = ?");
@ -1628,6 +1674,8 @@ app.put("/api/user/profile", requireApiAuth, async (req, res) => {
if (username && username !== user.username) changes.push("логин");
if (email !== undefined) changes.push("email");
if (accent_color !== undefined) changes.push("цвет темы");
if (show_edit_date !== undefined) changes.push("настройка показа даты редактирования");
if (colored_icons !== undefined) changes.push("цветные иконки");
if (newPassword) changes.push("пароль");
const details = `Обновлен профиль: ${changes.join(", ")}`;
logAction(userId, "profile_update", details);

View File

@ -85,7 +85,7 @@ define(['./workbox-8cfb3eb5'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.tihabijh3s"
"revision": "0.8irmmrfvp3g"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View File

@ -12,6 +12,8 @@ export const userApi = {
currentPassword?: string;
newPassword?: string;
accent_color?: string;
show_edit_date?: boolean;
colored_icons?: boolean;
}
) => {
const { data } = await axiosClient.put("/user/profile", profile);

View File

@ -43,6 +43,14 @@ export const Header: React.FC<HeaderProps> = ({
dispatch(setAccentColorAction(accent));
setAccentColor(accent);
// Применяем настройку цветных иконок
const coloredIcons = userData.colored_icons !== undefined ? userData.colored_icons === 1 : true;
if (coloredIcons) {
document.body.classList.add("colored-icons");
} else {
document.body.classList.remove("colored-icons");
}
// Загружаем AI настройки
try {
const aiSettings = await userApi.getAiSettings();

View File

@ -62,6 +62,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
const searchQuery = useAppSelector((state) => state.notes.searchQuery);
const isPreviewMode = useAppSelector((state) => state.ui.isPreviewMode);
const aiEnabled = useAppSelector((state) => state.profile.aiEnabled);
const user = useAppSelector((state) => state.profile.user);
const { showNotification } = useNotification();
const dispatch = useAppDispatch();
useMarkdown({ onNoteUpdate: onReload }); // Инициализируем обработчики спойлеров, внешних ссылок и чекбоксов
@ -914,6 +915,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
);
if (note.updated_at && note.created_at !== note.updated_at) {
const showEditDate = user?.show_edit_date !== undefined ? user.show_edit_date === 1 : true;
const updated = parseSQLiteUtc(note.updated_at);
const updatedStr = formatLocalDateTime(updated);
// Убеждаемся, что строка не содержит лишних символов
@ -921,17 +923,30 @@ export const NoteItem: React.FC<NoteItemProps> = ({
/(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})\d*.*/,
"$1"
);
return (
<>
{cleanCreatedStr}
<span className="date-separator"> | </span>
<Icon
icon="mdi:pencil"
style={{ fontSize: "12px", margin: "0 2px" }}
/>
{cleanUpdatedStr}
</>
);
if (showEditDate) {
return (
<>
{cleanCreatedStr}
<span className="date-separator"> | </span>
<Icon
icon="mdi:pencil"
style={{ fontSize: "12px", margin: "0 2px" }}
/>
{cleanUpdatedStr}
</>
);
} else {
return (
<>
{cleanCreatedStr}
<Icon
icon="mdi:pencil"
style={{ fontSize: "12px", margin: "0 2px" }}
/>
</>
);
}
} else {
return cleanCreatedStr;
}

View File

@ -29,6 +29,8 @@ const SettingsPage: React.FC = () => {
// Appearance settings
const [selectedAccentColor, setSelectedAccentColor] = useState("#007bff");
const [showEditDate, setShowEditDate] = useState(true);
const [coloredIcons, setColoredIcons] = useState(true);
// AI settings
const [apiKey, setApiKey] = useState("");
@ -84,6 +86,10 @@ const SettingsPage: React.FC = () => {
setSelectedAccentColor(accent);
dispatch(setAccentColorAction(accent));
setAccentColor(accent);
setShowEditDate(userData.show_edit_date !== undefined ? userData.show_edit_date === 1 : true);
const coloredIconsValue = userData.colored_icons !== undefined ? userData.colored_icons === 1 : true;
setColoredIcons(coloredIconsValue);
updateColoredIconsClass(coloredIconsValue);
// Загружаем AI настройки
try {
@ -114,13 +120,17 @@ const SettingsPage: React.FC = () => {
try {
await userApi.updateProfile({
accent_color: selectedAccentColor,
show_edit_date: showEditDate,
colored_icons: coloredIcons,
});
dispatch(setAccentColorAction(selectedAccentColor));
setAccentColor(selectedAccentColor);
await loadUserInfo();
showNotification("Цветовой акцент успешно обновлен", "success");
// Обновляем класс на body для управления цветными иконками
updateColoredIconsClass(coloredIcons);
showNotification("Настройки внешнего вида успешно обновлены", "success");
} catch (error: any) {
console.error("Ошибка обновления цветового акцента:", error);
console.error("Ошибка обновления настроек внешнего вида:", error);
showNotification(
error.response?.data?.error || "Ошибка обновления",
"error"
@ -128,6 +138,14 @@ const SettingsPage: React.FC = () => {
}
};
const updateColoredIconsClass = (enabled: boolean) => {
if (enabled) {
document.body.classList.add("colored-icons");
} else {
document.body.classList.remove("colored-icons");
}
};
const handleUpdateAiSettings = async () => {
if (!apiKey.trim()) {
showNotification("API ключ обязателен", "error");
@ -420,6 +438,55 @@ const SettingsPage: React.FC = () => {
</div>
</div>
<div className="form-group ai-toggle-group">
<label className="ai-toggle-label">
<div className="toggle-label-content">
<span className="toggle-text-main">Показывать дату редактирования</span>
<span className="toggle-text-desc">
{showEditDate
? "Отображать дату последнего редактирования заметки рядом с датой создания"
: "Показывать только иконку карандаша без даты редактирования"}
</span>
</div>
<div className="toggle-switch-wrapper">
<input
type="checkbox"
id="show-edit-date-toggle"
className="toggle-checkbox"
checked={showEditDate}
onChange={(e) => setShowEditDate(e.target.checked)}
/>
<span className="toggle-slider"></span>
</div>
</label>
</div>
<div className="form-group ai-toggle-group">
<label className="ai-toggle-label">
<div className="toggle-label-content">
<span className="toggle-text-main">Цветные иконки</span>
<span className="toggle-text-desc">
{coloredIcons
? "Иконки отображаются разными цветами для лучшей визуальной дифференциации"
: "Все иконки отображаются в монохромном стиле"}
</span>
</div>
<div className="toggle-switch-wrapper">
<input
type="checkbox"
id="colored-icons-toggle"
className="toggle-checkbox"
checked={coloredIcons}
onChange={(e) => {
setColoredIcons(e.target.checked);
updateColoredIconsClass(e.target.checked);
}}
/>
<span className="toggle-slider"></span>
</div>
</label>
</div>
<button className="btnSave" onClick={handleUpdateAppearance}>
Сохранить изменения
</button>

View File

@ -179,26 +179,178 @@ header .iconify {
margin-right: 6px;
}
/* Цветные иконки */
/* Иконка поиска - синий */
.search-title .iconify[data-icon="mdi:magnify"] {
color: var(--icon-search);
/* Цветные иконки - для кнопок заметок и заголовков страниц */
.colored-icons .iconify svg,
.colored-icons iconify-icon svg {
fill: currentColor !important;
}
/* Иконка тегов - зеленый */
.tags-title .iconify[data-icon="mdi:tag"] {
color: var(--icon-tags);
/* Универсальное правило для всех SVG в заголовках */
.colored-icons header svg,
.colored-icons header img[role="img"] svg {
fill: currentColor !important;
}
/* Иконка заметок - оранжевый */
header .iconify[data-icon="mdi:note-text"] {
color: var(--icon-notes);
/* Иконки в заголовках страниц */
/* Мои заметки - оранжевый */
.colored-icons .notes-header-left .iconify,
.colored-icons .notes-header-left iconify-icon,
.colored-icons header .notes-header-left .iconify,
.colored-icons header .notes-header-left iconify-icon,
.colored-icons header .notes-header-left svg,
.colored-icons header .notes-header span .iconify,
.colored-icons header .notes-header span iconify-icon,
.colored-icons header .notes-header span svg,
.colored-icons .notes-header-left img[src*="data:image/svg"],
.colored-icons header .notes-header-left img[src*="data:image/svg"],
.colored-icons header .notes-header span img[src*="data:image/svg"] {
color: #ff9800 !important;
fill: #ff9800 !important;
}
/* Иконка пользователя - фиолетовый */
header .iconify[data-icon="mdi:account"],
.username-clickable .iconify[data-icon="mdi:account"] {
color: var(--icon-user);
.colored-icons .notes-header-left svg,
.colored-icons header .notes-header-left svg,
.colored-icons header .notes-header span svg,
.colored-icons .notes-header-left img svg,
.colored-icons header .notes-header-left img svg {
fill: #ff9800 !important;
}
/* Все SVG внутри notes-header-left */
.colored-icons .notes-header-left svg path,
.colored-icons header .notes-header-left svg path {
fill: #ff9800 !important;
}
/* Настройки - серый/синий */
.colored-icons .settings-icon-btn .iconify,
.colored-icons .settings-icon-btn iconify-icon,
.colored-icons header button.settings-icon-btn .iconify,
.colored-icons header button.settings-icon-btn iconify-icon,
.colored-icons .settings-icon-btn svg,
.colored-icons header button.settings-icon-btn svg,
.colored-icons .settings-icon-btn svg path,
.colored-icons header button.settings-icon-btn svg path {
color: #607d8b !important;
fill: #607d8b !important;
}
/* Профиль - фиолетовый */
.colored-icons .user-avatar-placeholder-mini .iconify,
.colored-icons .user-avatar-placeholder-mini iconify-icon,
.colored-icons header .user-avatar-placeholder-mini .iconify,
.colored-icons header .user-avatar-placeholder-mini iconify-icon,
.colored-icons .user-avatar-placeholder-mini svg,
.colored-icons header .user-avatar-placeholder-mini svg,
.colored-icons .user-avatar-placeholder-mini svg path,
.colored-icons header .user-avatar-placeholder-mini svg path {
color: #9c27b0 !important;
fill: #9c27b0 !important;
}
/* Выйти - красный */
.colored-icons .logout-btn .iconify,
.colored-icons .logout-btn iconify-icon,
.colored-icons header button.logout-btn .iconify,
.colored-icons header button.logout-btn iconify-icon,
.colored-icons .logout-btn svg,
.colored-icons header button.logout-btn svg,
.colored-icons .logout-btn svg path,
.colored-icons header button.logout-btn svg path {
color: #f44336 !important;
fill: #f44336 !important;
}
/* Вход - синий */
.colored-icons header .iconify[data-icon="mdi:login"],
.colored-icons header iconify-icon[icon="mdi:login"] {
color: #2196f3 !important;
fill: #2196f3 !important;
}
/* Регистрация - зеленый */
.colored-icons header .iconify[data-icon="mdi:account-plus"],
.colored-icons header iconify-icon[icon="mdi:account-plus"] {
color: #4caf50 !important;
fill: #4caf50 !important;
}
/* Заголовок Настройки - серый/синий */
.colored-icons header.notes-header span:first-child .iconify,
.colored-icons header.notes-header span:first-child iconify-icon,
.colored-icons header.notes-header span:first-child svg,
.colored-icons header.notes-header span:first-child svg path {
color: #607d8b !important;
fill: #607d8b !important;
}
/* Кнопки в заголовке настроек */
.colored-icons header .notes-btn .iconify,
.colored-icons header .notes-btn iconify-icon,
.colored-icons header button.notes-btn .iconify,
.colored-icons header button.notes-btn iconify-icon,
.colored-icons header .notes-btn svg,
.colored-icons header button.notes-btn svg,
.colored-icons header .notes-btn svg path,
.colored-icons header button.notes-btn svg path {
color: #ff9800 !important;
fill: #ff9800 !important;
}
.colored-icons header .profile-btn .iconify,
.colored-icons header .profile-btn iconify-icon,
.colored-icons header button.profile-btn .iconify,
.colored-icons header button.profile-btn iconify-icon,
.colored-icons header .profile-btn svg,
.colored-icons header button.profile-btn svg,
.colored-icons header .profile-btn svg path,
.colored-icons header button.profile-btn svg path {
color: #9c27b0 !important;
fill: #9c27b0 !important;
}
/* Табы в настройках */
.colored-icons .settings-tab .iconify[data-icon="mdi:palette"],
.colored-icons .settings-tab iconify-icon[icon="mdi:palette"] {
color: #e91e63 !important;
fill: #e91e63 !important;
}
.colored-icons .settings-tab .iconify[data-icon="mdi:robot"],
.colored-icons .settings-tab iconify-icon[icon="mdi:robot"] {
color: #ff6f00 !important;
fill: #ff6f00 !important;
}
.colored-icons .settings-tab .iconify[data-icon="mdi:archive"],
.colored-icons .settings-tab iconify-icon[icon="mdi:archive"] {
color: #ff9800 !important;
fill: #ff9800 !important;
}
.colored-icons .settings-tab .iconify[data-icon="mdi:history"],
.colored-icons .settings-tab iconify-icon[icon="mdi:history"] {
color: #9c27b0 !important;
fill: #9c27b0 !important;
}
/* Иконки кнопок заметок - цветные при включенной настройке */
.colored-icons .note-actions .notesHeaderBtn:nth-child(1) .iconify,
.colored-icons .note-actions .notesHeaderBtn:nth-child(1) iconify-icon {
color: #ff9800 !important;
fill: #ff9800 !important;
}
.colored-icons .note-actions .notesHeaderBtn:nth-child(2) .iconify,
.colored-icons .note-actions .notesHeaderBtn:nth-child(2) iconify-icon {
color: #2196f3 !important;
fill: #2196f3 !important;
}
.colored-icons .note-actions .notesHeaderBtn:nth-child(3) .iconify,
.colored-icons .note-actions .notesHeaderBtn:nth-child(3) iconify-icon {
color: #dc3545 !important;
fill: #dc3545 !important;
}
/* Переключатель темы */
@ -235,97 +387,6 @@ header .iconify[data-icon="mdi:account"],
color: white;
}
/* Иконка входа - синий */
header .iconify[data-icon="mdi:login"] {
color: #2196f3;
}
/* Иконка регистрации - зеленый */
header .iconify[data-icon="mdi:account-plus"] {
color: #4caf50;
}
/* Markdown кнопки - разные цвета */
.btnMarkdown .iconify[data-icon="mdi:format-bold"] {
color: #d32f2f;
}
.btnMarkdown .iconify[data-icon="mdi:format-italic"] {
color: #f57c00;
}
.btnMarkdown .iconify[data-icon="mdi:palette"] {
color: #e91e63;
}
.btnMarkdown .iconify[data-icon="mdi:format-header-1"] {
color: #1976d2;
}
.btnMarkdown .iconify[data-icon="mdi:format-header-pound"] {
color: #1976d2;
}
.btnMarkdown .iconify[data-icon="mdi:format-list-bulleted"] {
color: #388e3c;
}
.btnMarkdown .iconify[data-icon="mdi:format-quote-close"] {
color: #f57c00;
}
.btnMarkdown .iconify[data-icon="mdi:code-tags"] {
color: #7b1fa2;
}
.btnMarkdown .iconify[data-icon="mdi:link"] {
color: #0288d1;
}
.btnMarkdown .iconify[data-icon="mdi:format-strikethrough"] {
color: #ff6f00;
}
.btnMarkdown .iconify[data-icon="mdi:format-list-numbered"] {
color: #4caf50;
}
.btnMarkdown .iconify[data-icon="mdi:checkbox-marked-outline"] {
color: #ff9800;
}
.btnMarkdown .iconify[data-icon="mdi:image-plus"] {
color: #2196f3;
}
.btnMarkdown .iconify[data-icon="mdi:eye"] {
color: #607d8b;
}
.btnMarkdown .iconify[data-icon="mdi:file-plus"] {
color: #9c27b0;
}
.btnMarkdown .iconify[data-icon="mdi:eye-off"] {
color: #e91e63;
}
.btnMarkdown .iconify[data-icon="mdi:robot"] {
color: #ff6f00;
}
.btnMarkdown .iconify[data-icon="mdi:menu-down"] {
color: #1976d2;
}
.notesHeaderBtn .iconify[data-icon="mdi:pencil"] {
color: #2196f3;
}
.notesHeaderBtn .iconify[data-icon="mdi:delete"] {
color: #dc3545;
}
header {
font-size: 20px;
font-weight: bold;
@ -1534,18 +1595,33 @@ textarea:focus {
font-size: 18px;
}
.floating-toolbar-btn .iconify[data-icon="mdi:format-bold"] {
.colored-icons .floating-toolbar-btn .iconify[data-icon="mdi:format-bold"],
.colored-icons .floating-toolbar-btn iconify-icon[icon="mdi:format-bold"] {
color: #1976d2;
}
.floating-toolbar-btn .iconify[data-icon="mdi:format-italic"] {
.colored-icons .floating-toolbar-btn .iconify[data-icon="mdi:format-italic"],
.colored-icons .floating-toolbar-btn iconify-icon[icon="mdi:format-italic"] {
color: #7b1fa2;
}
.floating-toolbar-btn .iconify[data-icon="mdi:format-strikethrough"] {
.colored-icons
.floating-toolbar-btn
.iconify[data-icon="mdi:format-strikethrough"],
.colored-icons
.floating-toolbar-btn
iconify-icon[icon="mdi:format-strikethrough"] {
color: #ff6f00;
}
/* Универсальный стиль для всех цветных иконок через SVG */
.colored-icons .iconify svg,
.colored-icons iconify-icon svg {
fill: currentColor;
}
/* Монохромные иконки для плавающей панели уже покрыты универсальным селектором */
.floating-toolbar-btn.active {
background-color: var(--bg-quaternary);
box-shadow: 0 0 0 2px var(--border-focus);
@ -4847,8 +4923,7 @@ textarea:focus {
}
/* Очень маленькие экраны */
@media (max-width: 480px) {
.mobile-menu-btn .iconify {
font-size: 20px;
}

View File

@ -3,6 +3,8 @@ export interface User {
email?: string;
avatar?: string;
accent_color: string;
show_edit_date?: number;
colored_icons?: number;
}
export interface AuthResponse {