Compare commits
8 Commits
a5f4e87056
...
d1a3853449
| Author | SHA1 | Date | |
|---|---|---|---|
| d1a3853449 | |||
| 4d91b2227d | |||
| 87a01629ae | |||
| 5b3e41d1b6 | |||
| 4b3bc3e024 | |||
| a78d976bcf | |||
| 2ec93b8cc2 | |||
| 3c2b23c699 |
35
.gitignore
vendored
35
.gitignore
vendored
@ -14,8 +14,20 @@ 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
|
||||||
@ -26,6 +38,7 @@ 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/*
|
||||||
@ -35,7 +48,6 @@ backend/public/uploads/*
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
@ -47,4 +59,25 @@ 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
|
||||||
|
|
||||||
|
|||||||
@ -158,10 +158,78 @@
|
|||||||
|
|
||||||
<!-- Manifest -->
|
<!-- Manifest -->
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<script type="module" crossorigin src="/assets/index-CRKRzJj1.js"></script>
|
<script type="module" crossorigin src="/assets/index-Czo9BXMw.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-QEK5TGz3.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-d4V1QKN4.css">
|
||||||
<link rel="manifest" href="/manifest.webmanifest"></head>
|
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root">
|
||||||
|
<!-- Индикатор загрузки до монтирования React -->
|
||||||
|
<div id="initial-loading" style="
|
||||||
|
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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -9,27 +9,10 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
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} didn’t 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")});
|
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} didn’t 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")});
|
||||||
|
|||||||
@ -203,6 +203,43 @@ 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 часа
|
||||||
@ -793,19 +830,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", "notes.html"));
|
return res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const accentColor = user?.accent_color || "#007bff";
|
const accentColor = user?.accent_color || "#007bff";
|
||||||
|
|
||||||
// Читаем HTML файл
|
// Читаем HTML файл
|
||||||
fs.readFile(
|
fs.readFile(
|
||||||
path.join(__dirname, "public", "notes.html"),
|
path.join(__dirname, "public", "index.html"),
|
||||||
"utf8",
|
"utf8",
|
||||||
(err, html) => {
|
(err, html) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка чтения файла notes.html:", err.message);
|
console.error("Ошибка чтения файла index.html:", err.message);
|
||||||
return res.sendFile(path.join(__dirname, "public", "notes.html"));
|
return res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
|
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
|
||||||
@ -1559,19 +1596,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", "profile.html"));
|
return res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const accentColor = user?.accent_color || "#007bff";
|
const accentColor = user?.accent_color || "#007bff";
|
||||||
|
|
||||||
// Читаем HTML файл
|
// Читаем HTML файл
|
||||||
fs.readFile(
|
fs.readFile(
|
||||||
path.join(__dirname, "public", "profile.html"),
|
path.join(__dirname, "public", "index.html"),
|
||||||
"utf8",
|
"utf8",
|
||||||
(err, html) => {
|
(err, html) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка чтения файла profile.html:", err.message);
|
console.error("Ошибка чтения файла index.html:", err.message);
|
||||||
return res.sendFile(path.join(__dirname, "public", "profile.html"));
|
return res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
|
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
|
||||||
@ -2243,19 +2280,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", "settings.html"));
|
return res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const accentColor = user?.accent_color || "#007bff";
|
const accentColor = user?.accent_color || "#007bff";
|
||||||
|
|
||||||
// Читаем HTML файл
|
// Читаем HTML файл
|
||||||
fs.readFile(
|
fs.readFile(
|
||||||
path.join(__dirname, "public", "settings.html"),
|
path.join(__dirname, "public", "index.html"),
|
||||||
"utf8",
|
"utf8",
|
||||||
(err, html) => {
|
(err, html) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка чтения файла settings.html:", err.message);
|
console.error("Ошибка чтения файла index.html:", err.message);
|
||||||
return res.sendFile(path.join(__dirname, "public", "settings.html"));
|
return res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вставляем inline CSS с правильным цветом
|
// Вставляем inline CSS с правильным цветом
|
||||||
|
|||||||
@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.nsn25edhihg"
|
"revision": "0.q31tuubm2tg"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
format,
|
format,
|
||||||
startOfMonth,
|
startOfMonth,
|
||||||
|
|||||||
@ -2,7 +2,6 @@ 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 {
|
||||||
|
|||||||
@ -296,6 +296,7 @@ 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}
|
||||||
@ -367,22 +368,6 @@ 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>
|
||||||
|
|||||||
@ -113,7 +113,14 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
};
|
};
|
||||||
}, [isDragging]);
|
}, [isDragging]);
|
||||||
|
|
||||||
const buttons = [];
|
const buttons: Array<{
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
before?: string;
|
||||||
|
after?: string;
|
||||||
|
action?: () => void;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -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, useAppDispatch } from "../../store/hooks";
|
import { useAppSelector } 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,7 +31,6 @@ 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()) {
|
||||||
@ -239,6 +238,119 @@ 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;
|
||||||
|
|
||||||
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
||||||
@ -408,14 +520,12 @@ 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;
|
||||||
}
|
}
|
||||||
@ -546,7 +656,6 @@ 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;
|
|
||||||
|
|
||||||
// Более точный расчет ширины символа
|
// Более точный расчет ширины символа
|
||||||
// Создаем временный элемент для измерения
|
// Создаем временный элемент для измерения
|
||||||
|
|||||||
@ -31,7 +31,6 @@ interface NoteItemProps {
|
|||||||
|
|
||||||
export const NoteItem: React.FC<NoteItemProps> = ({
|
export const NoteItem: React.FC<NoteItemProps> = ({
|
||||||
note,
|
note,
|
||||||
onDelete,
|
|
||||||
onPin,
|
onPin,
|
||||||
onArchive,
|
onArchive,
|
||||||
onReload,
|
onReload,
|
||||||
@ -140,20 +139,24 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
setLocalPreviewMode(false);
|
setLocalPreviewMode(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteExistingImage = (imageId: number) => {
|
const handleDeleteExistingImage = (imageId: number | string) => {
|
||||||
setDeletedImageIds([...deletedImageIds, imageId]);
|
const id = typeof imageId === 'number' ? imageId : Number(imageId);
|
||||||
|
setDeletedImageIds([...deletedImageIds, id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteExistingFile = (fileId: number) => {
|
const handleDeleteExistingFile = (fileId: number | string) => {
|
||||||
setDeletedFileIds([...deletedFileIds, fileId]);
|
const id = typeof fileId === 'number' ? fileId : Number(fileId);
|
||||||
|
setDeletedFileIds([...deletedFileIds, id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRestoreImage = (imageId: number) => {
|
const handleRestoreImage = (imageId: number | string) => {
|
||||||
setDeletedImageIds(deletedImageIds.filter((id) => id !== imageId));
|
const id = typeof imageId === 'number' ? imageId : Number(imageId);
|
||||||
|
setDeletedImageIds(deletedImageIds.filter((deletedId) => deletedId !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRestoreFile = (fileId: number) => {
|
const handleRestoreFile = (fileId: number | string) => {
|
||||||
setDeletedFileIds(deletedFileIds.filter((id) => id !== fileId));
|
const id = typeof fileId === 'number' ? fileId : Number(fileId);
|
||||||
|
setDeletedFileIds(deletedFileIds.filter((deletedId) => deletedId !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAiImprove = async () => {
|
const handleAiImprove = async () => {
|
||||||
@ -325,6 +328,119 @@ 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;
|
||||||
|
|
||||||
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
||||||
@ -506,7 +622,6 @@ 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;
|
|
||||||
|
|
||||||
// Более точный расчет ширины символа
|
// Более точный расчет ширины символа
|
||||||
// Создаем временный элемент для измерения
|
// Создаем временный элемент для измерения
|
||||||
@ -629,14 +744,12 @@ 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;
|
||||||
}
|
}
|
||||||
@ -1183,12 +1296,17 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="image-preview-list">
|
<div className="image-preview-list">
|
||||||
{note.images
|
{note.images
|
||||||
.filter((image) => !deletedImageIds.includes(image.id))
|
.filter((image) => {
|
||||||
|
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,
|
||||||
note.id,
|
noteId,
|
||||||
image.id
|
imageId
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div key={image.id} className="image-preview-item">
|
<div key={image.id} className="image-preview-item">
|
||||||
@ -1222,12 +1340,17 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="image-preview-list">
|
<div className="image-preview-list">
|
||||||
{note.images
|
{note.images
|
||||||
.filter((image) => deletedImageIds.includes(image.id))
|
.filter((image) => {
|
||||||
|
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,
|
||||||
note.id,
|
noteId,
|
||||||
image.id
|
imageId
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div key={image.id} className="image-preview-item">
|
<div key={image.id} className="image-preview-item">
|
||||||
@ -1262,13 +1385,11 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="file-preview-list">
|
<div className="file-preview-list">
|
||||||
{note.files
|
{note.files
|
||||||
.filter((file) => !deletedFileIds.includes(file.id))
|
.filter((file) => {
|
||||||
|
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
|
||||||
@ -1308,7 +1429,10 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="file-preview-list">
|
<div className="file-preview-list">
|
||||||
{note.files
|
{note.files
|
||||||
.filter((file) => deletedFileIds.includes(file.id))
|
.filter((file) => {
|
||||||
|
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">
|
||||||
@ -1402,10 +1526,12 @@ 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,
|
||||||
note.id,
|
noteId,
|
||||||
image.id
|
imageId
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div key={image.id} className="note-image-item">
|
<div key={image.id} className="note-image-item">
|
||||||
@ -1427,7 +1553,9 @@ 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 fileUrl = getFileUrl(file.file_path, note.id, file.id);
|
const noteId = typeof note.id === 'number' ? note.id : Number(note.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
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useImperativeHandle, forwardRef } from "react";
|
import { 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);
|
||||||
|
|||||||
@ -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<NodeJS.Timeout | null>(null);
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Debounce для поиска
|
// Debounce для поиска
|
||||||
|
|||||||
@ -10,7 +10,8 @@ export const useNotification = () => {
|
|||||||
message: string,
|
message: string,
|
||||||
type: "info" | "success" | "error" | "warning" = "info"
|
type: "info" | "success" | "error" | "warning" = "info"
|
||||||
) => {
|
) => {
|
||||||
const id = dispatch(addNotification({ message, type })).payload.id;
|
const action = dispatch(addNotification({ message, type }));
|
||||||
|
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);
|
||||||
|
|||||||
@ -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 { useAppSelector, useAppDispatch } from "../store/hooks";
|
import { 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,7 +16,6 @@ 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("");
|
||||||
|
|||||||
@ -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 { useAppSelector, useAppDispatch } from "../store/hooks";
|
import { 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,7 +13,6 @@ 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";
|
||||||
@ -25,8 +24,6 @@ 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 при инициализации
|
||||||
@ -162,11 +159,16 @@ const SettingsPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleUpdateAppearance = async () => {
|
const handleUpdateAppearance = async () => {
|
||||||
try {
|
try {
|
||||||
await userApi.updateProfile({
|
const profileUpdate: any = {
|
||||||
accent_color: selectedAccentColor,
|
accent_color: selectedAccentColor,
|
||||||
show_edit_date: showEditDate,
|
};
|
||||||
colored_icons: coloredIcons,
|
if (showEditDate !== undefined) {
|
||||||
});
|
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();
|
||||||
@ -420,7 +422,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
|
|
||||||
// Загружаем версию из IndexedDB
|
// Загружаем версию из IndexedDB
|
||||||
try {
|
try {
|
||||||
const userId = user?.id;
|
const userId = (await import("../store")).store.getState().auth.userId;
|
||||||
const localVer = userId
|
const localVer = userId
|
||||||
? await dbManager.getDataVersionByUserId(userId)
|
? await dbManager.getDataVersionByUserId(userId)
|
||||||
: await dbManager.getDataVersion();
|
: await dbManager.getDataVersion();
|
||||||
@ -837,14 +839,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(note.id)}
|
onClick={() => handleRestoreNote(typeof note.id === 'number' ? note.id : Number(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(note.id)}
|
onClick={() => handleDeletePermanent(typeof note.id === 'number' ? note.id : Number(note.id))}
|
||||||
title="Удалить навсегда"
|
title="Удалить навсегда"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:delete-forever" /> Удалить
|
<Icon icon="mdi:delete-forever" /> Удалить
|
||||||
|
|||||||
@ -5,13 +5,11 @@ 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';
|
||||||
|
|
||||||
@ -20,7 +18,7 @@ const RETRY_DELAY_MS = 5000;
|
|||||||
|
|
||||||
class SyncService {
|
class SyncService {
|
||||||
private isSyncing = false;
|
private isSyncing = false;
|
||||||
private syncTimer: NodeJS.Timeout | null = null;
|
private syncTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
private listeners: Array<() => void> = [];
|
private listeners: Array<() => void> = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -444,7 +442,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);
|
||||||
@ -461,7 +459,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);
|
||||||
|
|||||||
@ -1612,22 +1612,29 @@ 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;
|
||||||
@ -1638,7 +1645,6 @@ 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;
|
||||||
@ -1669,6 +1675,28 @@ 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 {
|
||||||
|
|||||||
@ -45,10 +45,23 @@ 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" : ""} />`;
|
||||||
return `<li class="task-list-item">${checkbox} ${text}</li>\n`;
|
return `<li class="task-list-item">${checkbox} ${text}</li>\n`;
|
||||||
|
|||||||
@ -143,7 +143,8 @@ export default defineConfig({
|
|||||||
registerType: "prompt",
|
registerType: "prompt",
|
||||||
devOptions: {
|
devOptions: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
type: "module",
|
type: "classic",
|
||||||
|
navigateFallback: "index.html",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user