Compare commits

..

No commits in common. "d1a3853449c2a5f3385f590a834eac9a8c7b9f42" and "a5f4e87056b73536303c2f0bea257931042c8d7a" have entirely different histories.

21 changed files with 255 additions and 646 deletions

35
.gitignore vendored
View File

@ -14,20 +14,8 @@ backend/node_modules
# Build outputs # Build outputs
dist dist
dist-ssr dist-ssr
dev-dist
*.local *.local
# Vite
.vite
vite.config.*.timestamp-*
# Build artifacts in backend/public
backend/public/assets/
backend/public/workbox-*.js
backend/public/registerSW.js
backend/public/sw.js
backend/public/index.html
# Environment variables # Environment variables
.env .env
.env.local .env.local
@ -38,7 +26,6 @@ backend/.env
backend/database/*.db backend/database/*.db
backend/database/*.db-shm backend/database/*.db-shm
backend/database/*.db-wal backend/database/*.db-wal
backend/database/*.db-journal
# Uploads # Uploads
backend/public/uploads/* backend/public/uploads/*
@ -48,6 +35,7 @@ backend/public/uploads/*
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea
.DS_Store
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
@ -59,25 +47,4 @@ public/sw.js.map
# OS # OS
Thumbs.db Thumbs.db
.DS_Store
*.swp
*.swo
*~
# TypeScript
*.tsbuildinfo
.tsbuildinfo
# Coverage
coverage
*.lcov
.nyc_output
# Testing
.jest
# Misc
.cache
.parcel-cache
.turbo

View File

@ -1,69 +1,69 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<title>NoteJS - Система заметок</title> <title>NoteJS - Система заметок</title>
<!-- Предотвращение мерцания темы --> <!-- Предотвращение мерцания темы -->
<script> <script>
(function () { (function () {
try { try {
// Получаем сохраненную тему // Получаем сохраненную тему
const savedTheme = localStorage.getItem("theme"); const savedTheme = localStorage.getItem("theme");
// Получаем системные предпочтения // Получаем системные предпочтения
const systemPrefersDark = window.matchMedia( const systemPrefersDark = window.matchMedia(
"(prefers-color-scheme: dark)" "(prefers-color-scheme: dark)"
).matches; ).matches;
// Определяем тему: сохраненная или системная // Определяем тему: сохраненная или системная
const theme = savedTheme || (systemPrefersDark ? "dark" : "light"); const theme = savedTheme || (systemPrefersDark ? "dark" : "light");
// Функция для конвертации hex в RGB // Функция для конвертации hex в RGB
function hexToRgb(hex) { function hexToRgb(hex) {
const cleanHex = hex.replace("#", ""); const cleanHex = hex.replace("#", "");
const r = parseInt(cleanHex.substring(0, 2), 16); const r = parseInt(cleanHex.substring(0, 2), 16);
const g = parseInt(cleanHex.substring(2, 4), 16); const g = parseInt(cleanHex.substring(2, 4), 16);
const b = parseInt(cleanHex.substring(4, 6), 16); const b = parseInt(cleanHex.substring(4, 6), 16);
return `${r}, ${g}, ${b}`; return `${r}, ${g}, ${b}`;
} }
// Получаем и устанавливаем accentColor // Получаем и устанавливаем accentColor
const savedAccentColor = localStorage.getItem("accentColor"); const savedAccentColor = localStorage.getItem("accentColor");
const accentColor = savedAccentColor || "#007bff"; const accentColor = savedAccentColor || "#007bff";
// Устанавливаем тему и переменные до загрузки CSS // Устанавливаем тему и переменные до загрузки CSS
if (theme === "dark") { if (theme === "dark") {
document.documentElement.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark");
} else { } else {
document.documentElement.setAttribute("data-theme", "light"); document.documentElement.setAttribute("data-theme", "light");
} }
// Устанавливаем CSS переменные для accent цвета // Устанавливаем CSS переменные для accent цвета
document.documentElement.style.setProperty("--accent-color", accentColor); document.documentElement.style.setProperty("--accent-color", accentColor);
document.documentElement.style.setProperty("--accent-color-rgb", hexToRgb(accentColor)); document.documentElement.style.setProperty("--accent-color-rgb", hexToRgb(accentColor));
// Устанавливаем цвет для meta theme-color // Устанавливаем цвет для meta theme-color
const themeColorMeta = document.querySelector('meta[name="theme-color"]'); const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta) { if (themeColorMeta) {
themeColorMeta.setAttribute( themeColorMeta.setAttribute(
"content", "content",
theme === "dark" ? "#1a1a1a" : accentColor theme === "dark" ? "#1a1a1a" : accentColor
); );
} }
} catch (e) { } catch (e) {
// В случае ошибки устанавливаем светлую тему по умолчанию // В случае ошибки устанавливаем светлую тему по умолчанию
document.documentElement.setAttribute("data-theme", "light"); document.documentElement.setAttribute("data-theme", "light");
document.documentElement.style.setProperty("--accent-color", "#007bff"); document.documentElement.style.setProperty("--accent-color", "#007bff");
document.documentElement.style.setProperty("--accent-color-rgb", "0, 123, 255"); document.documentElement.style.setProperty("--accent-color-rgb", "0, 123, 255");
} }
})(); })();
</script> </script>
<!-- Критические стили темы для предотвращения flash эффекта --> <!-- Критические стили темы для предотвращения flash эффекта -->
<style> <style>
:root { :root {
--accent-color: #007bff; --accent-color: #007bff;
@ -89,147 +89,79 @@
color: var(--text-primary); color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
} }
</style> </style>
<!-- PWA Meta Tags --> <!-- PWA Meta Tags -->
<meta <meta
name="description" name="description"
content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря"
/> />
<meta name="theme-color" content="#007bff" /> <meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta <meta
name="apple-mobile-web-app-status-bar-style" name="apple-mobile-web-app-status-bar-style"
content="black-translucent" content="black-translucent"
/> />
<meta name="apple-mobile-web-app-title" content="NoteJS" /> <meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="apple-touch-fullscreen" content="yes" /> <meta name="apple-touch-fullscreen" content="yes" />
<meta name="msapplication-TileColor" content="#007bff" /> <meta name="msapplication-TileColor" content="#007bff" />
<meta name="msapplication-config" content="/browserconfig.xml" /> <meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" /> <meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
<meta name="application-name" content="NoteJS" /> <meta name="application-name" content="NoteJS" />
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
<!-- Icons --> <!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" /> <link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="32x32" sizes="32x32"
href="/icons/icon-32x32.png" href="/icons/icon-32x32.png"
/> />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="16x16" sizes="16x16"
href="/icons/icon-16x16.png" href="/icons/icon-16x16.png"
/> />
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" /> <link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" /> <link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" /> <link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" /> <link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="114x114" sizes="114x114"
href="/icons/icon-128x128.png" href="/icons/icon-128x128.png"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="120x120" sizes="120x120"
href="/icons/icon-128x128.png" href="/icons/icon-128x128.png"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="144x144" sizes="144x144"
href="/icons/icon-144x144.png" href="/icons/icon-144x144.png"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="152x152" sizes="152x152"
href="/icons/icon-152x152.png" href="/icons/icon-152x152.png"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="180x180" sizes="180x180"
href="/icons/icon-192x192.png" href="/icons/icon-192x192.png"
/> />
<link rel="mask-icon" href="/icon.svg" color="#007bff" /> <link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest --> <!-- Manifest -->
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<script type="module" crossorigin src="/assets/index-Czo9BXMw.js"></script> <script type="module" crossorigin src="/assets/index-CRKRzJj1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-d4V1QKN4.css"> <link rel="stylesheet" crossorigin href="/assets/index-QEK5TGz3.css">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head> <link rel="manifest" href="/manifest.webmanifest"></head>
<body> <body>
<div id="root"> <div id="root"></div>
<!-- Индикатор загрузки до монтирования React --> </body>
<div id="initial-loading" style=" </html>
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--bg-primary);
color: var(--text-primary);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
">
<style>
#initial-loading-spinner {
width: 50px;
height: 50px;
border: 4px solid transparent;
border-top: 4px solid var(--accent-color, #007bff);
border-right: 4px solid var(--accent-color, #007bff);
border-bottom: 4px solid transparent;
border-left: 4px solid transparent;
border-radius: 50%;
animation: initial-loading-spin 0.8s linear infinite;
opacity: 0.8;
}
@keyframes initial-loading-spin {
to { transform: rotate(360deg); }
}
</style>
<div id="initial-loading-spinner"></div>
</div>
</div>
<script>
// Скрываем индикатор загрузки сразу после загрузки DOM
// React удалит этот элемент при первом рендере через createRoot
(function() {
// Используем MutationObserver для отслеживания изменений в #root
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// Если React начал добавлять элементы в #root, удаляем индикатор
if (mutation.addedNodes.length > 0) {
const loadingEl = document.getElementById('initial-loading');
if (loadingEl && loadingEl.parentNode) {
loadingEl.parentNode.removeChild(loadingEl);
}
observer.disconnect();
}
});
});
// Начинаем наблюдение за изменениями в #root
const root = document.getElementById('root');
if (root) {
observer.observe(root, { childList: true, subtree: true });
// Фолбэк: если через 2 секунды элемент все еще есть, удаляем вручную
setTimeout(function() {
const loadingEl = document.getElementById('initial-loading');
if (loadingEl && loadingEl.parentNode) {
loadingEl.parentNode.removeChild(loadingEl);
}
observer.disconnect();
}, 2000);
}
})();
</script>
</body>
</html>

View File

@ -9,10 +9,27 @@
"orientation": "portrait-primary", "orientation": "portrait-primary",
"scope": "/", "scope": "/",
"lang": "ru", "lang": "ru",
"id": "/",
"categories": ["productivity", "utilities"], "categories": ["productivity", "utilities"],
"prefer_related_applications": false, "prefer_related_applications": false,
"display_override": ["window-controls-overlay", "standalone"], "display_override": ["window-controls-overlay", "standalone"],
"dir": "ltr", "dir": "ltr",
"shortcuts": [
{
"name": "Новая заметка",
"short_name": "Новая",
"description": "Создать новую заметку",
"url": "/notes?new=true",
"icons": [{ "src": "/icons/icon-192x192.png", "sizes": "192x192" }]
},
{
"name": "Мой профиль",
"short_name": "Профиль",
"description": "Открыть профиль",
"url": "/profile",
"icons": [{ "src": "/icons/icon-192x192.png", "sizes": "192x192" }]
}
],
"screenshots": [ "screenshots": [
{ {
"src": "/icons/icon-512x512.png", "src": "/icons/icon-512x512.png",

View File

@ -1 +1 @@
if(!self.define){let e,n={};const i=(i,c)=>(i=new URL(i+".js",c).href,n[i]||new Promise(n=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=n,document.head.appendChild(e)}else e=i,importScripts(i),n()}).then(()=>{let e=n[i];if(!e)throw new Error(`Module ${i} didnt register its module`);return e}));self.define=(c,o)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(n[s])return;let a={};const r=e=>i(e,s),d={module:{uri:s},exports:a,require:r};n[s]=Promise.all(c.map(e=>d[e]||r(e))).then(e=>(o(...e),a))}}define(["./workbox-e20531c6"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-Czo9BXMw.js",revision:null},{url:"assets/index-d4V1QKN4.css",revision:null},{url:"icon.svg",revision:"0ec61aab261526d4c491e887a6f3374e"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"b8f278b7bd4e55f30369e5de187d6eaa"},{url:"logo.svg",revision:"5962d0d24d9cd26cd8aaff9cb6f54a5a"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"icon.svg",revision:"0ec61aab261526d4c491e887a6f3374e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html"))),e.registerRoute(/^https:\/\/api\./,new e.NetworkFirst({cacheName:"api-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:3600})]}),"GET"),e.registerRoute(/\/api\//,new e.NetworkFirst({cacheName:"api-cache-local",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/uploads\//,new e.CacheFirst({cacheName:"uploads-cache",plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/,new e.CacheFirst({cacheName:"images-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")}); if(!self.define){let e,i={};const n=(n,c)=>(n=new URL(n+".js",c).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didnt register its module`);return e}));self.define=(c,o)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let r={};const d=e=>n(e,s),a={module:{uri:s},exports:r,require:d};i[s]=Promise.all(c.map(e=>a[e]||d(e))).then(e=>(o(...e),r))}}define(["./workbox-57555046"],function(e){"use strict";self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.precacheAndRoute([{url:"assets/index-CRKRzJj1.js",revision:null},{url:"assets/index-QEK5TGz3.css",revision:null},{url:"assets/workbox-window.prod.es5-B9K5rw8f.js",revision:null},{url:"icon.svg",revision:"537ae73d8f9e90e6a01816aa6d527d16"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-16x16.png",revision:"101c13808e9fd0956f247bc446a8ac1e"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-32x32.png",revision:"22ee5d42535bc339ab0e19cb496378a5"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-48x48.png",revision:"cfdd3bebd931375f2e0277d638ec8781"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"index.html",revision:"52c85beb0841c0c7c8ddf774370cff39"},{url:"logo.svg",revision:"11616ede8898b4c24203e331b3ec6dc3"},{url:"icons/icon-72x72.png",revision:"6b3cb1b2537ec91921698260a9c2f47c"},{url:"icons/icon-96x96.png",revision:"7efd757a81217207d981de88ef199d86"},{url:"icons/icon-128x128.png",revision:"fa71db17e345406d5f7d847f88c65ac4"},{url:"icons/icon-144x144.png",revision:"e790ff42758ea1a2a46eb84201630757"},{url:"icons/icon-152x152.png",revision:"88f2400f6617a32cc9cd62c70fb49a05"},{url:"icons/icon-192x192.png",revision:"7d86d2d2ada99d7cee015dff0fdcb497"},{url:"icons/icon-384x384.png",revision:"c601fa602952a903389e5e8f8a699617"},{url:"icons/icon-512x512.png",revision:"8731edef999b9e7deba310d72a739925"},{url:"manifest.webmanifest",revision:"1c071cadebd7a1b0dc1eeb0270e73fb8"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html"))),e.registerRoute(/^https:\/\/api\./i,new e.NetworkFirst({cacheName:"api-cache",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:png|jpg|jpeg|svg|gif|webp)$/i,new e.CacheFirst({cacheName:"image-cache",plugins:[new e.ExpirationPlugin({maxEntries:100,maxAgeSeconds:2592e3})]}),"GET")});

View File

@ -203,43 +203,6 @@ app.get("/sw.js", (req, res) => {
res.sendFile(path.join(__dirname, "public", "sw.js")); res.sendFile(path.join(__dirname, "public", "sw.js"));
}); });
// Service worker для dev режима
const devDistPath = path.join(__dirname, "..", "dev-dist");
app.get("/dev-sw.js", (req, res) => {
res.setHeader("Content-Type", "application/javascript");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
const swPath = path.join(devDistPath, "sw.js");
if (fs.existsSync(swPath)) {
res.sendFile(swPath);
} else {
res.status(404).send("Service worker not found");
}
});
// RegisterSW для dev режима
app.get("/registerSW.js", (req, res) => {
res.setHeader("Content-Type", "application/javascript");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
const registerSWPath = path.join(devDistPath, "registerSW.js");
if (fs.existsSync(registerSWPath)) {
res.sendFile(registerSWPath);
} else {
res.status(404).send("registerSW.js not found");
}
});
// Workbox файлы для dev режима
app.get("/workbox-:hash.js", (req, res) => {
res.setHeader("Content-Type", "application/javascript");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
const workboxPath = path.join(devDistPath, `workbox-${req.params.hash}.js`);
if (fs.existsSync(workboxPath)) {
res.sendFile(workboxPath);
} else {
res.status(404).send("Workbox file not found");
}
});
app.get("/browserconfig.xml", (req, res) => { app.get("/browserconfig.xml", (req, res) => {
res.setHeader("Content-Type", "application/xml"); res.setHeader("Content-Type", "application/xml");
res.setHeader("Cache-Control", "public, max-age=86400"); // 24 часа res.setHeader("Cache-Control", "public, max-age=86400"); // 24 часа
@ -830,19 +793,19 @@ app.get("/notes", requireAuth, (req, res) => {
db.get(sql, [req.session.userId], (err, user) => { db.get(sql, [req.session.userId], (err, user) => {
if (err) { if (err) {
console.error("Ошибка получения цвета пользователя:", err.message); console.error("Ошибка получения цвета пользователя:", err.message);
return res.sendFile(path.join(__dirname, "public", "index.html")); return res.sendFile(path.join(__dirname, "public", "notes.html"));
} }
const accentColor = user?.accent_color || "#007bff"; const accentColor = user?.accent_color || "#007bff";
// Читаем HTML файл // Читаем HTML файл
fs.readFile( fs.readFile(
path.join(__dirname, "public", "index.html"), path.join(__dirname, "public", "notes.html"),
"utf8", "utf8",
(err, html) => { (err, html) => {
if (err) { if (err) {
console.error("Ошибка чтения файла index.html:", err.message); console.error("Ошибка чтения файла notes.html:", err.message);
return res.sendFile(path.join(__dirname, "public", "index.html")); return res.sendFile(path.join(__dirname, "public", "notes.html"));
} }
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC // Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
@ -1596,19 +1559,19 @@ app.get("/profile", requireAuth, (req, res) => {
db.get(sql, [req.session.userId], (err, user) => { db.get(sql, [req.session.userId], (err, user) => {
if (err) { if (err) {
console.error("Ошибка получения цвета пользователя:", err.message); console.error("Ошибка получения цвета пользователя:", err.message);
return res.sendFile(path.join(__dirname, "public", "index.html")); return res.sendFile(path.join(__dirname, "public", "profile.html"));
} }
const accentColor = user?.accent_color || "#007bff"; const accentColor = user?.accent_color || "#007bff";
// Читаем HTML файл // Читаем HTML файл
fs.readFile( fs.readFile(
path.join(__dirname, "public", "index.html"), path.join(__dirname, "public", "profile.html"),
"utf8", "utf8",
(err, html) => { (err, html) => {
if (err) { if (err) {
console.error("Ошибка чтения файла index.html:", err.message); console.error("Ошибка чтения файла profile.html:", err.message);
return res.sendFile(path.join(__dirname, "public", "index.html")); return res.sendFile(path.join(__dirname, "public", "profile.html"));
} }
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC // Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
@ -2280,19 +2243,19 @@ app.get("/settings", requireAuth, (req, res) => {
db.get(sql, [req.session.userId], (err, user) => { db.get(sql, [req.session.userId], (err, user) => {
if (err) { if (err) {
console.error("Ошибка получения цвета пользователя:", err.message); console.error("Ошибка получения цвета пользователя:", err.message);
return res.sendFile(path.join(__dirname, "public", "index.html")); return res.sendFile(path.join(__dirname, "public", "settings.html"));
} }
const accentColor = user?.accent_color || "#007bff"; const accentColor = user?.accent_color || "#007bff";
// Читаем HTML файл // Читаем HTML файл
fs.readFile( fs.readFile(
path.join(__dirname, "public", "index.html"), path.join(__dirname, "public", "settings.html"),
"utf8", "utf8",
(err, html) => { (err, html) => {
if (err) { if (err) {
console.error("Ошибка чтения файла index.html:", err.message); console.error("Ошибка чтения файла settings.html:", err.message);
return res.sendFile(path.join(__dirname, "public", "index.html")); return res.sendFile(path.join(__dirname, "public", "settings.html"));
} }
// Вставляем inline CSS с правильным цветом // Вставляем inline CSS с правильным цветом

View File

@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.q31tuubm2tg" "revision": "0.nsn25edhihg"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { import {
format, format,
startOfMonth, startOfMonth,

View File

@ -2,6 +2,7 @@ import React from "react";
import { MiniCalendar } from "../calendar/MiniCalendar"; import { MiniCalendar } from "../calendar/MiniCalendar";
import { SearchBar } from "../search/SearchBar"; import { SearchBar } from "../search/SearchBar";
import { TagsFilter } from "../search/TagsFilter"; import { TagsFilter } from "../search/TagsFilter";
import { useAppSelector } from "../../store/hooks";
import { Note } from "../../types/note"; import { Note } from "../../types/note";
interface SidebarProps { interface SidebarProps {

View File

@ -296,7 +296,6 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
{hasSelection && ( {hasSelection && (
<> <>
<div className="floating-toolbar-separator" />
<button <button
className="floating-toolbar-btn" className="floating-toolbar-btn"
onClick={handleCopy} onClick={handleCopy}
@ -368,6 +367,22 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
> >
<Icon icon="mdi:eye-off" /> <Icon icon="mdi:eye-off" />
</button> </button>
<button
className="floating-toolbar-btn"
onClick={() => handleFormat("`", "`")}
title="Код"
>
<Icon icon="mdi:code-tags" />
</button>
<button
className="floating-toolbar-btn"
onClick={() => handleFormat("> ", "")}
title="Цитата"
>
<Icon icon="mdi:format-quote-close" />
</button>
</> </>
)} )}
</div> </div>

View File

@ -113,14 +113,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
}; };
}, [isDragging]); }, [isDragging]);
const buttons: Array<{ const buttons = [];
id: string;
icon: string;
title: string;
before?: string;
after?: string;
action?: () => void;
}> = [];
return ( return (
<div <div

View File

@ -4,7 +4,7 @@ import { FloatingToolbar } from "./FloatingToolbar";
import { NotePreview } from "./NotePreview"; import { NotePreview } from "./NotePreview";
import { ImageUpload } from "./ImageUpload"; import { ImageUpload } from "./ImageUpload";
import { FileUpload } from "./FileUpload"; import { FileUpload } from "./FileUpload";
import { useAppSelector } from "../../store/hooks"; import { useAppSelector, useAppDispatch } from "../../store/hooks";
import { useNotification } from "../../hooks/useNotification"; import { useNotification } from "../../hooks/useNotification";
import { offlineNotesApi } from "../../api/offlineNotesApi"; import { offlineNotesApi } from "../../api/offlineNotesApi";
import { aiApi } from "../../api/aiApi"; import { aiApi } from "../../api/aiApi";
@ -31,6 +31,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
const isPreviewMode = useAppSelector((state) => state.ui.isPreviewMode); const isPreviewMode = useAppSelector((state) => state.ui.isPreviewMode);
const { showNotification } = useNotification(); const { showNotification } = useNotification();
const aiEnabled = useAppSelector((state) => state.profile.aiEnabled); const aiEnabled = useAppSelector((state) => state.profile.aiEnabled);
const dispatch = useAppDispatch();
const handleSave = async () => { const handleSave = async () => {
if (!content.trim()) { if (!content.trim()) {
@ -238,119 +239,6 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
const end = textarea.selectionEnd; const end = textarea.selectionEnd;
const selectedText = content.substring(start, end); const selectedText = content.substring(start, end);
// Определяем маркеры списков и цитат, которые обрабатываются построчно
const listMarkers = ["- ", "1. ", "- [ ] ", "> "];
const isListMarker = listMarkers.includes(before);
// Если это маркер списка и выделено несколько строк, обрабатываем построчно
if (isListMarker && selectedText.includes("\n")) {
const lines = selectedText.split("\n");
const beforeText = content.substring(0, start);
const afterText = content.substring(end);
// Определяем, есть ли уже такие маркеры на всех строках
let allLinesHaveMarker = true;
for (const line of lines) {
const trimmedLine = line.trimStart();
if (before === "- ") {
// Для маркированного списка проверяем различные варианты
if (!trimmedLine.match(/^[-*+]\s/)) {
allLinesHaveMarker = false;
}
} else if (before === "1. ") {
// Для нумерованного списка
if (!trimmedLine.match(/^\d+\.\s/)) {
allLinesHaveMarker = false;
}
} else if (before === "- [ ] ") {
// Для чекбокса
if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
allLinesHaveMarker = false;
}
} else if (before === "> ") {
// Для цитаты
if (!trimmedLine.startsWith("> ")) {
allLinesHaveMarker = false;
}
}
}
// Если все строки уже имеют маркер, удаляем их (переключение)
// Если некоторые имеют, но не все - добавляем к тем, у которых нет
const processedLines = lines.map((line, index) => {
const trimmedLine = line.trimStart();
const leadingSpaces = line.substring(0, line.length - trimmedLine.length);
let shouldToggle = false;
if (before === "- ") {
const match = trimmedLine.match(/^([-*+])\s/);
if (match) {
shouldToggle = true;
}
} else if (before === "1. ") {
if (trimmedLine.match(/^\d+\.\s/)) {
shouldToggle = true;
}
} else if (before === "- [ ] ") {
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
shouldToggle = true;
}
} else if (before === "> ") {
if (trimmedLine.startsWith("> ")) {
shouldToggle = true;
}
}
if (shouldToggle && allLinesHaveMarker) {
// Удаляем маркер
if (before === "- ") {
const match = trimmedLine.match(/^([-*+])\s(.*)$/);
return match ? leadingSpaces + match[2] : line;
} else if (before === "1. ") {
const match = trimmedLine.match(/^\d+\.\s(.*)$/);
return match ? leadingSpaces + match[1] : line;
} else if (before === "- [ ] ") {
const match = trimmedLine.match(/^-\s+\[[ xX]\]\s(.*)$/);
return match ? leadingSpaces + match[1] : line;
} else if (before === "> ") {
return trimmedLine.startsWith("> ")
? leadingSpaces + trimmedLine.substring(2)
: line;
}
} else if (!shouldToggle || !allLinesHaveMarker) {
// Добавляем маркер
if (before === "1. ") {
// Для нумерованного списка добавляем правильный номер
const number = index + 1;
return leadingSpaces + `${number}. ` + trimmedLine;
} else {
return leadingSpaces + before + trimmedLine;
}
}
return line;
});
const newSelectedText = processedLines.join("\n");
const newText = beforeText + newSelectedText + afterText;
// Вычисляем новую позицию курсора
const newStart = start;
const newEnd = start + newSelectedText.length;
setContent(newText);
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(newStart, newEnd);
const formats = getActiveFormats();
setActiveFormats(formats);
}, 0);
return;
}
const tagLength = before.length; const tagLength = before.length;
// Проверяем область вокруг выделения (расширяем для проверки тегов) // Проверяем область вокруг выделения (расширяем для проверки тегов)
@ -520,12 +408,14 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
const lines = text.split("\n"); const lines = text.split("\n");
// Определяем текущую строку // Определяем текущую строку
let currentLineIndex = 0;
let currentLineStart = 0; let currentLineStart = 0;
let currentLine = ""; let currentLine = "";
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length; const lineLength = lines[i].length;
if (currentLineStart + lineLength >= start) { if (currentLineStart + lineLength >= start) {
currentLineIndex = i;
currentLine = lines[i]; currentLine = lines[i];
break; break;
} }
@ -656,6 +546,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
const lineHeight = parseInt(styles.lineHeight) || 20; const lineHeight = parseInt(styles.lineHeight) || 20;
const paddingTop = parseInt(styles.paddingTop) || 0; const paddingTop = parseInt(styles.paddingTop) || 0;
const paddingLeft = parseInt(styles.paddingLeft) || 0; const paddingLeft = parseInt(styles.paddingLeft) || 0;
const fontSize = parseInt(styles.fontSize) || 14;
// Более точный расчет ширины символа // Более точный расчет ширины символа
// Создаем временный элемент для измерения // Создаем временный элемент для измерения

View File

@ -31,6 +31,7 @@ interface NoteItemProps {
export const NoteItem: React.FC<NoteItemProps> = ({ export const NoteItem: React.FC<NoteItemProps> = ({
note, note,
onDelete,
onPin, onPin,
onArchive, onArchive,
onReload, onReload,
@ -139,24 +140,20 @@ export const NoteItem: React.FC<NoteItemProps> = ({
setLocalPreviewMode(false); setLocalPreviewMode(false);
}; };
const handleDeleteExistingImage = (imageId: number | string) => { const handleDeleteExistingImage = (imageId: number) => {
const id = typeof imageId === 'number' ? imageId : Number(imageId); setDeletedImageIds([...deletedImageIds, imageId]);
setDeletedImageIds([...deletedImageIds, id]);
}; };
const handleDeleteExistingFile = (fileId: number | string) => { const handleDeleteExistingFile = (fileId: number) => {
const id = typeof fileId === 'number' ? fileId : Number(fileId); setDeletedFileIds([...deletedFileIds, fileId]);
setDeletedFileIds([...deletedFileIds, id]);
}; };
const handleRestoreImage = (imageId: number | string) => { const handleRestoreImage = (imageId: number) => {
const id = typeof imageId === 'number' ? imageId : Number(imageId); setDeletedImageIds(deletedImageIds.filter((id) => id !== imageId));
setDeletedImageIds(deletedImageIds.filter((deletedId) => deletedId !== id));
}; };
const handleRestoreFile = (fileId: number | string) => { const handleRestoreFile = (fileId: number) => {
const id = typeof fileId === 'number' ? fileId : Number(fileId); setDeletedFileIds(deletedFileIds.filter((id) => id !== fileId));
setDeletedFileIds(deletedFileIds.filter((deletedId) => deletedId !== id));
}; };
const handleAiImprove = async () => { const handleAiImprove = async () => {
@ -328,119 +325,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
const end = textarea.selectionEnd; const end = textarea.selectionEnd;
const selectedText = editContent.substring(start, end); const selectedText = editContent.substring(start, end);
// Определяем маркеры списков и цитат, которые обрабатываются построчно
const listMarkers = ["- ", "1. ", "- [ ] ", "> "];
const isListMarker = listMarkers.includes(before);
// Если это маркер списка и выделено несколько строк, обрабатываем построчно
if (isListMarker && selectedText.includes("\n")) {
const lines = selectedText.split("\n");
const beforeText = editContent.substring(0, start);
const afterText = editContent.substring(end);
// Определяем, есть ли уже такие маркеры на всех строках
let allLinesHaveMarker = true;
for (const line of lines) {
const trimmedLine = line.trimStart();
if (before === "- ") {
// Для маркированного списка проверяем различные варианты
if (!trimmedLine.match(/^[-*+]\s/)) {
allLinesHaveMarker = false;
}
} else if (before === "1. ") {
// Для нумерованного списка
if (!trimmedLine.match(/^\d+\.\s/)) {
allLinesHaveMarker = false;
}
} else if (before === "- [ ] ") {
// Для чекбокса
if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
allLinesHaveMarker = false;
}
} else if (before === "> ") {
// Для цитаты
if (!trimmedLine.startsWith("> ")) {
allLinesHaveMarker = false;
}
}
}
// Если все строки уже имеют маркер, удаляем их (переключение)
// Если некоторые имеют, но не все - добавляем к тем, у которых нет
const processedLines = lines.map((line, index) => {
const trimmedLine = line.trimStart();
const leadingSpaces = line.substring(0, line.length - trimmedLine.length);
let shouldToggle = false;
if (before === "- ") {
const match = trimmedLine.match(/^([-*+])\s/);
if (match) {
shouldToggle = true;
}
} else if (before === "1. ") {
if (trimmedLine.match(/^\d+\.\s/)) {
shouldToggle = true;
}
} else if (before === "- [ ] ") {
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
shouldToggle = true;
}
} else if (before === "> ") {
if (trimmedLine.startsWith("> ")) {
shouldToggle = true;
}
}
if (shouldToggle && allLinesHaveMarker) {
// Удаляем маркер
if (before === "- ") {
const match = trimmedLine.match(/^([-*+])\s(.*)$/);
return match ? leadingSpaces + match[2] : line;
} else if (before === "1. ") {
const match = trimmedLine.match(/^\d+\.\s(.*)$/);
return match ? leadingSpaces + match[1] : line;
} else if (before === "- [ ] ") {
const match = trimmedLine.match(/^-\s+\[[ xX]\]\s(.*)$/);
return match ? leadingSpaces + match[1] : line;
} else if (before === "> ") {
return trimmedLine.startsWith("> ")
? leadingSpaces + trimmedLine.substring(2)
: line;
}
} else if (!shouldToggle || !allLinesHaveMarker) {
// Добавляем маркер
if (before === "1. ") {
// Для нумерованного списка добавляем правильный номер
const number = index + 1;
return leadingSpaces + `${number}. ` + trimmedLine;
} else {
return leadingSpaces + before + trimmedLine;
}
}
return line;
});
const newSelectedText = processedLines.join("\n");
const newText = beforeText + newSelectedText + afterText;
// Вычисляем новую позицию курсора
const newStart = start;
const newEnd = start + newSelectedText.length;
setEditContent(newText);
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(newStart, newEnd);
const formats = getActiveFormats();
setActiveFormats(formats);
}, 0);
return;
}
const tagLength = before.length; const tagLength = before.length;
// Проверяем область вокруг выделения (расширяем для проверки тегов) // Проверяем область вокруг выделения (расширяем для проверки тегов)
@ -622,6 +506,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
const lineHeight = parseInt(styles.lineHeight) || 20; const lineHeight = parseInt(styles.lineHeight) || 20;
const paddingTop = parseInt(styles.paddingTop) || 0; const paddingTop = parseInt(styles.paddingTop) || 0;
const paddingLeft = parseInt(styles.paddingLeft) || 0; const paddingLeft = parseInt(styles.paddingLeft) || 0;
const fontSize = parseInt(styles.fontSize) || 14;
// Более точный расчет ширины символа // Более точный расчет ширины символа
// Создаем временный элемент для измерения // Создаем временный элемент для измерения
@ -744,12 +629,14 @@ export const NoteItem: React.FC<NoteItemProps> = ({
const lines = text.split("\n"); const lines = text.split("\n");
// Определяем текущую строку // Определяем текущую строку
let currentLineIndex = 0;
let currentLineStart = 0; let currentLineStart = 0;
let currentLine = ""; let currentLine = "";
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length; const lineLength = lines[i].length;
if (currentLineStart + lineLength >= start) { if (currentLineStart + lineLength >= start) {
currentLineIndex = i;
currentLine = lines[i]; currentLine = lines[i];
break; break;
} }
@ -1296,17 +1183,12 @@ export const NoteItem: React.FC<NoteItemProps> = ({
</div> </div>
<div className="image-preview-list"> <div className="image-preview-list">
{note.images {note.images
.filter((image) => { .filter((image) => !deletedImageIds.includes(image.id))
const imageId = typeof image.id === 'number' ? image.id : Number(image.id);
return !deletedImageIds.includes(imageId);
})
.map((image) => { .map((image) => {
const noteId = typeof note.id === 'number' ? note.id : Number(note.id);
const imageId = typeof image.id === 'number' ? image.id : Number(image.id);
const imageUrl = getImageUrl( const imageUrl = getImageUrl(
image.file_path, image.file_path,
noteId, note.id,
imageId image.id
); );
return ( return (
<div key={image.id} className="image-preview-item"> <div key={image.id} className="image-preview-item">
@ -1340,17 +1222,12 @@ export const NoteItem: React.FC<NoteItemProps> = ({
</div> </div>
<div className="image-preview-list"> <div className="image-preview-list">
{note.images {note.images
.filter((image) => { .filter((image) => deletedImageIds.includes(image.id))
const imageId = typeof image.id === 'number' ? image.id : Number(image.id);
return deletedImageIds.includes(imageId);
})
.map((image) => { .map((image) => {
const noteId = typeof note.id === 'number' ? note.id : Number(note.id);
const imageId = typeof image.id === 'number' ? image.id : Number(image.id);
const imageUrl = getImageUrl( const imageUrl = getImageUrl(
image.file_path, image.file_path,
noteId, note.id,
imageId image.id
); );
return ( return (
<div key={image.id} className="image-preview-item"> <div key={image.id} className="image-preview-item">
@ -1385,11 +1262,13 @@ export const NoteItem: React.FC<NoteItemProps> = ({
</div> </div>
<div className="file-preview-list"> <div className="file-preview-list">
{note.files {note.files
.filter((file) => { .filter((file) => !deletedFileIds.includes(file.id))
const fileId = typeof file.id === 'number' ? file.id : Number(file.id);
return !deletedFileIds.includes(fileId);
})
.map((file) => { .map((file) => {
const fileUrl = getFileUrl(
file.file_path,
note.id,
file.id
);
return ( return (
<div key={file.id} className="file-preview-item"> <div key={file.id} className="file-preview-item">
<Icon <Icon
@ -1429,10 +1308,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
</div> </div>
<div className="file-preview-list"> <div className="file-preview-list">
{note.files {note.files
.filter((file) => { .filter((file) => deletedFileIds.includes(file.id))
const fileId = typeof file.id === 'number' ? file.id : Number(file.id);
return deletedFileIds.includes(fileId);
})
.map((file) => { .map((file) => {
return ( return (
<div key={file.id} className="file-preview-item"> <div key={file.id} className="file-preview-item">
@ -1526,12 +1402,10 @@ export const NoteItem: React.FC<NoteItemProps> = ({
{note.images && note.images.length > 0 && ( {note.images && note.images.length > 0 && (
<div className="note-images-container"> <div className="note-images-container">
{note.images.map((image) => { {note.images.map((image) => {
const noteId = typeof note.id === 'number' ? note.id : Number(note.id);
const imageId = typeof image.id === 'number' ? image.id : Number(image.id);
const imageUrl = getImageUrl( const imageUrl = getImageUrl(
image.file_path, image.file_path,
noteId, note.id,
imageId image.id
); );
return ( return (
<div key={image.id} className="note-image-item"> <div key={image.id} className="note-image-item">
@ -1553,9 +1427,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
{note.files && note.files.length > 0 && ( {note.files && note.files.length > 0 && (
<div className="note-files-container"> <div className="note-files-container">
{note.files.map((file) => { {note.files.map((file) => {
const noteId = typeof note.id === 'number' ? note.id : Number(note.id); const fileUrl = getFileUrl(file.file_path, note.id, file.id);
const fileId = typeof file.id === 'number' ? file.id : Number(file.id);
const fileUrl = getFileUrl(file.file_path, noteId, fileId);
return ( return (
<div key={file.id} className="note-file-item"> <div key={file.id} className="note-file-item">
<a <a

View File

@ -1,4 +1,4 @@
import { useEffect, useImperativeHandle, forwardRef } from "react"; import React, { useEffect, useImperativeHandle, forwardRef } from "react";
import { NoteItem } from "./NoteItem"; import { NoteItem } from "./NoteItem";
import { useAppSelector, useAppDispatch } from "../../store/hooks"; import { useAppSelector, useAppDispatch } from "../../store/hooks";
import { offlineNotesApi } from "../../api/offlineNotesApi"; import { offlineNotesApi } from "../../api/offlineNotesApi";
@ -10,7 +10,7 @@ export interface NotesListRef {
reloadNotes: () => void; reloadNotes: () => void;
} }
export const NotesList = forwardRef<NotesListRef>((_props, ref) => { export const NotesList = forwardRef<NotesListRef>((props, ref) => {
const notes = useAppSelector((state) => state.notes.notes); const notes = useAppSelector((state) => state.notes.notes);
const userId = useAppSelector((state) => state.auth.userId); const userId = useAppSelector((state) => state.auth.userId);
const searchQuery = useAppSelector((state) => state.notes.searchQuery); const searchQuery = useAppSelector((state) => state.notes.searchQuery);

View File

@ -6,7 +6,7 @@ import { setSearchQuery } from "../../store/slices/notesSlice";
export const SearchBar: React.FC = () => { export const SearchBar: React.FC = () => {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
// Debounce для поиска // Debounce для поиска

View File

@ -10,8 +10,7 @@ export const useNotification = () => {
message: string, message: string,
type: "info" | "success" | "error" | "warning" = "info" type: "info" | "success" | "error" | "warning" = "info"
) => { ) => {
const action = dispatch(addNotification({ message, type })); const id = dispatch(addNotification({ message, type })).payload.id;
const id = (action as any).payload?.id || `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
setTimeout(() => { setTimeout(() => {
dispatch(removeNotification(id)); dispatch(removeNotification(id));
}, 4000); }, 4000);

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { useAppDispatch } from "../store/hooks"; import { useAppSelector, useAppDispatch } from "../store/hooks";
import { userApi } from "../api/userApi"; import { userApi } from "../api/userApi";
import { authApi } from "../api/authApi"; import { authApi } from "../api/authApi";
import { clearAuth } from "../store/slices/authSlice"; import { clearAuth } from "../store/slices/authSlice";
@ -16,6 +16,7 @@ const ProfilePage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { showNotification } = useNotification(); const { showNotification } = useNotification();
const user = useAppSelector((state) => state.profile.user);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { useAppDispatch } from "../store/hooks"; import { useAppSelector, useAppDispatch } from "../store/hooks";
import { userApi } from "../api/userApi"; import { userApi } from "../api/userApi";
import { authApi } from "../api/authApi"; import { authApi } from "../api/authApi";
import { notesApi, logsApi, Log } from "../api/notesApi"; import { notesApi, logsApi, Log } from "../api/notesApi";
@ -13,6 +13,7 @@ import { setAccentColor } from "../utils/colorUtils";
import { useNotification } from "../hooks/useNotification"; import { useNotification } from "../hooks/useNotification";
import { Modal } from "../components/common/Modal"; import { Modal } from "../components/common/Modal";
import { ThemeToggle } from "../components/common/ThemeToggle"; import { ThemeToggle } from "../components/common/ThemeToggle";
import { formatDateFromTimestamp } from "../utils/dateFormat";
import { parseMarkdown } from "../utils/markdown"; import { parseMarkdown } from "../utils/markdown";
import { dbManager } from "../utils/indexedDB"; import { dbManager } from "../utils/indexedDB";
import { syncService } from "../services/syncService"; import { syncService } from "../services/syncService";
@ -24,6 +25,8 @@ const SettingsPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { showNotification } = useNotification(); const { showNotification } = useNotification();
const user = useAppSelector((state) => state.profile.user);
const accentColor = useAppSelector((state) => state.ui.accentColor);
const [activeTab, setActiveTab] = useState<SettingsTab>(() => { const [activeTab, setActiveTab] = useState<SettingsTab>(() => {
// Восстанавливаем активную вкладку из localStorage при инициализации // Восстанавливаем активную вкладку из localStorage при инициализации
@ -159,16 +162,11 @@ const SettingsPage: React.FC = () => {
const handleUpdateAppearance = async () => { const handleUpdateAppearance = async () => {
try { try {
const profileUpdate: any = { await userApi.updateProfile({
accent_color: selectedAccentColor, accent_color: selectedAccentColor,
}; show_edit_date: showEditDate,
if (showEditDate !== undefined) { colored_icons: coloredIcons,
profileUpdate.show_edit_date = showEditDate; });
}
if (coloredIcons !== undefined) {
profileUpdate.colored_icons = coloredIcons;
}
await userApi.updateProfile(profileUpdate);
dispatch(setAccentColorAction(selectedAccentColor)); dispatch(setAccentColorAction(selectedAccentColor));
setAccentColor(selectedAccentColor); setAccentColor(selectedAccentColor);
await loadUserInfo(); await loadUserInfo();
@ -422,7 +420,7 @@ const SettingsPage: React.FC = () => {
// Загружаем версию из IndexedDB // Загружаем версию из IndexedDB
try { try {
const userId = (await import("../store")).store.getState().auth.userId; const userId = user?.id;
const localVer = userId const localVer = userId
? await dbManager.getDataVersionByUserId(userId) ? await dbManager.getDataVersionByUserId(userId)
: await dbManager.getDataVersion(); : await dbManager.getDataVersion();
@ -839,14 +837,14 @@ const SettingsPage: React.FC = () => {
<div className="archived-note-actions"> <div className="archived-note-actions">
<button <button
className="btn-restore" className="btn-restore"
onClick={() => handleRestoreNote(typeof note.id === 'number' ? note.id : Number(note.id))} onClick={() => handleRestoreNote(note.id)}
title="Восстановить" title="Восстановить"
> >
<Icon icon="mdi:restore" /> Восстановить <Icon icon="mdi:restore" /> Восстановить
</button> </button>
<button <button
className="btn-delete-permanent" className="btn-delete-permanent"
onClick={() => handleDeletePermanent(typeof note.id === 'number' ? note.id : Number(note.id))} onClick={() => handleDeletePermanent(note.id)}
title="Удалить навсегда" title="Удалить навсегда"
> >
<Icon icon="mdi:delete-forever" /> Удалить <Icon icon="mdi:delete-forever" /> Удалить

View File

@ -5,11 +5,13 @@ import { Note, NoteImage, NoteFile } from '../types/note';
import { store } from '../store/index'; import { store } from '../store/index';
import { import {
setSyncStatus, setSyncStatus,
removeNotification,
addNotification, addNotification,
} from '../store/slices/uiSlice'; } from '../store/slices/uiSlice';
import { import {
updateNote, updateNote,
setPendingSyncCount, setPendingSyncCount,
setOfflineMode,
} from '../store/slices/notesSlice'; } from '../store/slices/notesSlice';
import { SyncQueueItem } from '../types/note'; import { SyncQueueItem } from '../types/note';
@ -18,7 +20,7 @@ const RETRY_DELAY_MS = 5000;
class SyncService { class SyncService {
private isSyncing = false; private isSyncing = false;
private syncTimer: ReturnType<typeof setTimeout> | null = null; private syncTimer: NodeJS.Timeout | null = null;
private listeners: Array<() => void> = []; private listeners: Array<() => void> = [];
/** /**
@ -442,7 +444,7 @@ class SyncService {
*/ */
private async updateImageReferences( private async updateImageReferences(
localNote: Note, localNote: Note,
_serverNote: Note serverNote: Note
): Promise<NoteImage[]> { ): Promise<NoteImage[]> {
// Если нет изображений с base64, возвращаем как есть // Если нет изображений с base64, возвращаем как есть
const hasBase64Images = localNote.images.some((img) => img.base64Data); const hasBase64Images = localNote.images.some((img) => img.base64Data);
@ -459,7 +461,7 @@ class SyncService {
*/ */
private async updateFileReferences( private async updateFileReferences(
localNote: Note, localNote: Note,
_serverNote: Note serverNote: Note
): Promise<NoteFile[]> { ): Promise<NoteFile[]> {
// Если нет файлов с base64, возвращаем как есть // Если нет файлов с base64, возвращаем как есть
const hasBase64Files = localNote.files.some((file) => file.base64Data); const hasBase64Files = localNote.files.some((file) => file.base64Data);

View File

@ -1612,29 +1612,22 @@ textarea:focus {
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
} }
/* Плавающая панель форматирования - улучшенная версия */ /* Плавающая панель форматирования */
.floating-toolbar-wrapper { .floating-toolbar-wrapper {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
/* Скрываем скроллбар */
scrollbar-width: none; scrollbar-width: none;
/* Плавная прокрутка */
scroll-behavior: smooth; scroll-behavior: smooth;
/* Максимальная ширина с учетом отступов */
max-width: calc(100vw - 20px); max-width: calc(100vw - 20px);
/* Предотвращаем выбор текста при перетаскивании */
user-select: none;
-webkit-user-select: none;
/* Улучшаем производительность */
will-change: transform;
} }
.floating-toolbar-wrapper::-webkit-scrollbar { .floating-toolbar-wrapper::-webkit-scrollbar {
display: none; display: none;
} }
/* Мобильная версия */
.floating-toolbar-wrapper.mobile {
max-width: calc(100vw - 32px);
}
.floating-toolbar { .floating-toolbar {
display: flex; display: flex;
gap: 4px; gap: 4px;
@ -1645,6 +1638,7 @@ textarea:focus {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: fadeIn 0.2s ease-out; animation: fadeIn 0.2s ease-out;
align-items: center; align-items: center;
/* Предотвращаем сжатие кнопок */
flex-shrink: 0; flex-shrink: 0;
min-width: fit-content; min-width: fit-content;
width: max-content; width: max-content;
@ -1675,28 +1669,6 @@ textarea:focus {
transition: all 0.2s ease; transition: all 0.2s ease;
min-width: 32px; min-width: 32px;
min-height: 32px; min-height: 32px;
/* Улучшение для touch */
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* Увеличенные кнопки на мобильных */
@media (max-width: 768px) {
.floating-toolbar-wrapper.mobile .floating-toolbar-btn {
min-width: 44px;
min-height: 44px;
padding: 8px 12px;
font-size: 18px;
}
.floating-toolbar-wrapper.mobile .floating-toolbar {
padding: 8px;
gap: 6px;
}
.floating-toolbar-wrapper.mobile .floating-toolbar-btn .iconify {
font-size: 20px;
}
} }
.floating-toolbar-btn:hover { .floating-toolbar-btn:hover {

View File

@ -45,22 +45,9 @@ const renderer: any = {
}, },
// Кастомный renderer для элементов списка с чекбоксами // Кастомный renderer для элементов списка с чекбоксами
listitem(token: any) { listitem(token: any) {
const text = token.text;
const task = token.task; const task = token.task;
const checked = token.checked; const checked = token.checked;
// Используем tokens для правильной обработки форматирования внутри элементов списка
// token.tokens содержит массив токенов для вложенного содержимого
const tokens = token.tokens || [];
let text: string;
if (tokens.length > 0) {
// Используем this.parser.parseInline для правильной обработки вложенного форматирования
// this указывает на экземпляр Parser в контексте renderer
text = this.parser.parseInline(tokens);
} else {
// Fallback на token.text, если tokens отсутствуют
text = token.text || '';
}
if (task) { if (task) {
const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} />`; const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} />`;

View File

@ -143,8 +143,7 @@ export default defineConfig({
registerType: "prompt", registerType: "prompt",
devOptions: { devOptions: {
enabled: true, enabled: true,
type: "classic", type: "module",
navigateFallback: "index.html",
}, },
}), }),
], ],