Вернул старый оффлайн режим когда он не работал
This commit is contained in:
parent
5b3e41d1b6
commit
87a01629ae
@ -81,34 +81,15 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
|
|||||||
"url": "registerSW.js",
|
"url": "registerSW.js",
|
||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "/index.html",
|
"url": "index.html",
|
||||||
"revision": "0.ri1maclacqo"
|
"revision": "0.fijdulj6fg"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
allowlist: [/^\/$/],
|
allowlist: [/^\/$/]
|
||||||
denylist: [/^\/api/, /^\/_/]
|
|
||||||
}));
|
}));
|
||||||
workbox.registerRoute(({
|
|
||||||
request
|
|
||||||
}) => request.destination === "document" || request.url.endsWith("/") || request.url.endsWith("/index.html"), new workbox.NetworkFirst({
|
|
||||||
"cacheName": "html-cache",
|
|
||||||
"networkTimeoutSeconds": 3,
|
|
||||||
plugins: [new workbox.ExpirationPlugin({
|
|
||||||
maxEntries: 10,
|
|
||||||
maxAgeSeconds: 86400
|
|
||||||
})]
|
|
||||||
}), 'GET');
|
|
||||||
workbox.registerRoute(/\.(?:js|css|woff|woff2|ttf|eot)$/, new workbox.CacheFirst({
|
|
||||||
"cacheName": "static-resources-cache",
|
|
||||||
plugins: [new workbox.ExpirationPlugin({
|
|
||||||
maxEntries: 200,
|
|
||||||
maxAgeSeconds: 31536000
|
|
||||||
})]
|
|
||||||
}), 'GET');
|
|
||||||
workbox.registerRoute(/^https:\/\/api\./, new workbox.NetworkFirst({
|
workbox.registerRoute(/^https:\/\/api\./, new workbox.NetworkFirst({
|
||||||
"cacheName": "api-cache",
|
"cacheName": "api-cache",
|
||||||
"networkTimeoutSeconds": 3,
|
|
||||||
plugins: [new workbox.ExpirationPlugin({
|
plugins: [new workbox.ExpirationPlugin({
|
||||||
maxEntries: 50,
|
maxEntries: 50,
|
||||||
maxAgeSeconds: 3600
|
maxAgeSeconds: 3600
|
||||||
@ -116,7 +97,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
|
|||||||
}), 'GET');
|
}), 'GET');
|
||||||
workbox.registerRoute(/\/api\//, new workbox.NetworkFirst({
|
workbox.registerRoute(/\/api\//, new workbox.NetworkFirst({
|
||||||
"cacheName": "api-cache-local",
|
"cacheName": "api-cache-local",
|
||||||
"networkTimeoutSeconds": 3,
|
"networkTimeoutSeconds": 10,
|
||||||
plugins: [new workbox.ExpirationPlugin({
|
plugins: [new workbox.ExpirationPlugin({
|
||||||
maxEntries: 100,
|
maxEntries: 100,
|
||||||
maxAgeSeconds: 86400
|
maxAgeSeconds: 86400
|
||||||
|
|||||||
@ -19,12 +19,7 @@ const AppContent: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<NotificationStack />
|
<NotificationStack />
|
||||||
<InstallPrompt />
|
<InstallPrompt />
|
||||||
<BrowserRouter
|
<BrowserRouter>
|
||||||
future={{
|
|
||||||
v7_startTransition: true,
|
|
||||||
v7_relativeSplatPath: true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<LoginPage />} />
|
<Route path="/" element={<LoginPage />} />
|
||||||
<Route path="/register" element={<RegisterPage />} />
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
|||||||
@ -34,31 +34,22 @@ axiosClient.interceptors.response.use(
|
|||||||
|
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
// Список URL, где 401 означает неправильный пароль, а не истечение сессии
|
// Список URL, где 401 означает неправильный пароль, а не истечение сессии
|
||||||
// или где не нужно делать редирект (например, проверка статуса)
|
|
||||||
const passwordProtectedUrls = [
|
const passwordProtectedUrls = [
|
||||||
"/login", // Страница входа
|
"/login", // Страница входа
|
||||||
"/register", // Страница регистрации
|
"/register", // Страница регистрации
|
||||||
"/notes/archived/all", // Удаление всех архивных заметок
|
"/notes/archived/all", // Удаление всех архивных заметок
|
||||||
"/user/delete-account", // Удаление аккаунта
|
"/user/delete-account", // Удаление аккаунта
|
||||||
"/auth/status", // Проверка статуса (не нужно делать редирект при ошибке)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Проверяем, является ли это запросом с проверкой пароля или статуса
|
// Проверяем, является ли это запросом с проверкой пароля
|
||||||
const isPasswordProtected = passwordProtectedUrls.some((url) =>
|
const isPasswordProtected = passwordProtectedUrls.some((url) =>
|
||||||
error.config?.url?.includes(url)
|
error.config?.url?.includes(url)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Разлогиниваем только если это НЕ запрос с проверкой пароля/статуса
|
// Разлогиниваем только если это НЕ запрос с проверкой пароля
|
||||||
// и только если есть ответ от сервера (не offline режим)
|
if (!isPasswordProtected) {
|
||||||
if (!isPasswordProtected && error.response) {
|
|
||||||
localStorage.removeItem("isAuthenticated");
|
localStorage.removeItem("isAuthenticated");
|
||||||
localStorage.removeItem("userId");
|
window.location.href = "/";
|
||||||
localStorage.removeItem("username");
|
|
||||||
// Не делаем редирект, если нет интернета (error.response будет undefined)
|
|
||||||
// ProtectedRoute сам обработает это
|
|
||||||
if (navigator.onLine) {
|
|
||||||
window.location.href = "/";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export const notesApi = {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
unarchive: async (id: number | string) => {
|
unarchive: async (id: number) => {
|
||||||
const { data } = await axiosClient.put(`/notes/${id}/unarchive`);
|
const { data } = await axiosClient.put(`/notes/${id}/unarchive`);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@ -89,7 +89,7 @@ export const notesApi = {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteArchived: async (id: number | string) => {
|
deleteArchived: async (id: number) => {
|
||||||
await axiosClient.delete(`/notes/archived/${id}`);
|
await axiosClient.delete(`/notes/archived/${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,8 @@ export const userApi = {
|
|||||||
currentPassword?: string;
|
currentPassword?: string;
|
||||||
newPassword?: string;
|
newPassword?: string;
|
||||||
accent_color?: string;
|
accent_color?: string;
|
||||||
show_edit_date?: boolean | number;
|
show_edit_date?: boolean;
|
||||||
colored_icons?: boolean | number;
|
colored_icons?: boolean;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { data } = await axiosClient.put("/user/profile", profile);
|
const { data } = await axiosClient.put("/user/profile", profile);
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { useAppSelector, useAppDispatch } from "../store/hooks";
|
|||||||
import { setAuth, clearAuth } from "../store/slices/authSlice";
|
import { setAuth, clearAuth } from "../store/slices/authSlice";
|
||||||
import { authApi } from "../api/authApi";
|
import { authApi } from "../api/authApi";
|
||||||
import { LoadingOverlay } from "./common/LoadingOverlay";
|
import { LoadingOverlay } from "./common/LoadingOverlay";
|
||||||
import { checkNetworkStatus } from "../utils/offlineManager";
|
|
||||||
|
|
||||||
export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
|
export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
@ -16,84 +15,26 @@ export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
try {
|
try {
|
||||||
// Проверяем, есть ли интернет
|
const authStatus = await authApi.checkStatus();
|
||||||
const isOnline = await checkNetworkStatus();
|
if (authStatus.authenticated) {
|
||||||
|
|
||||||
if (!isOnline) {
|
|
||||||
// Offline режим - используем данные из localStorage
|
|
||||||
const savedAuth = localStorage.getItem("isAuthenticated") === "true";
|
|
||||||
const savedUserId = localStorage.getItem("userId");
|
|
||||||
const savedUsername = localStorage.getItem("username");
|
|
||||||
|
|
||||||
if (savedAuth && savedUserId && savedUsername) {
|
|
||||||
// Восстанавливаем авторизацию из localStorage
|
|
||||||
dispatch(
|
|
||||||
setAuth({
|
|
||||||
userId: parseInt(savedUserId, 10),
|
|
||||||
username: savedUsername,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
console.log('[ProtectedRoute] Offline mode: restored auth from localStorage');
|
|
||||||
} else {
|
|
||||||
// Нет сохраненных данных - не авторизован
|
|
||||||
dispatch(clearAuth());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Online режим - проверяем через API
|
|
||||||
try {
|
|
||||||
const authStatus = await authApi.checkStatus();
|
|
||||||
if (authStatus.authenticated) {
|
|
||||||
dispatch(
|
|
||||||
setAuth({
|
|
||||||
userId: authStatus.userId!,
|
|
||||||
username: authStatus.username!,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
dispatch(clearAuth());
|
|
||||||
}
|
|
||||||
} catch (apiError) {
|
|
||||||
// Если API запрос не удался, но есть данные в localStorage
|
|
||||||
// Используем их для offline работы
|
|
||||||
const savedAuth = localStorage.getItem("isAuthenticated") === "true";
|
|
||||||
const savedUserId = localStorage.getItem("userId");
|
|
||||||
const savedUsername = localStorage.getItem("username");
|
|
||||||
|
|
||||||
if (savedAuth && savedUserId && savedUsername) {
|
|
||||||
console.log('[ProtectedRoute] API failed, using cached auth');
|
|
||||||
dispatch(
|
|
||||||
setAuth({
|
|
||||||
userId: parseInt(savedUserId, 10),
|
|
||||||
username: savedUsername,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
dispatch(clearAuth());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[ProtectedRoute] Auth check error:', error);
|
|
||||||
// В случае ошибки проверяем localStorage
|
|
||||||
const savedAuth = localStorage.getItem("isAuthenticated") === "true";
|
|
||||||
const savedUserId = localStorage.getItem("userId");
|
|
||||||
const savedUsername = localStorage.getItem("username");
|
|
||||||
|
|
||||||
if (savedAuth && savedUserId && savedUsername) {
|
|
||||||
dispatch(
|
dispatch(
|
||||||
setAuth({
|
setAuth({
|
||||||
userId: parseInt(savedUserId, 10),
|
userId: authStatus.userId!,
|
||||||
username: savedUsername,
|
username: authStatus.username!,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(clearAuth());
|
dispatch(clearAuth());
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
dispatch(clearAuth());
|
||||||
} finally {
|
} finally {
|
||||||
setIsChecking(false);
|
setIsChecking(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Всегда проверяем статус аутентификации при монтировании,
|
||||||
|
// независимо от начального состояния Redux (localStorage может быть устаревшим)
|
||||||
checkAuth();
|
checkAuth();
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
format,
|
format,
|
||||||
startOfMonth,
|
startOfMonth,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -113,14 +113,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
};
|
};
|
||||||
}, [isDragging]);
|
}, [isDragging]);
|
||||||
|
|
||||||
const buttons: Array<{
|
const buttons = [];
|
||||||
id: string;
|
|
||||||
action?: () => void;
|
|
||||||
before?: string;
|
|
||||||
after?: string;
|
|
||||||
title?: string;
|
|
||||||
icon?: string;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -149,7 +142,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
|||||||
}}
|
}}
|
||||||
title={btn.title}
|
title={btn.title}
|
||||||
>
|
>
|
||||||
{btn.icon && <Icon icon={btn.icon} />}
|
<Icon icon={btn.icon} />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@ -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()) {
|
||||||
@ -250,27 +251,36 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
|
|
||||||
// Определяем, есть ли уже такие маркеры на всех строках
|
// Определяем, есть ли уже такие маркеры на всех строках
|
||||||
let allLinesHaveMarker = true;
|
let allLinesHaveMarker = true;
|
||||||
|
let hasAnyMarker = false;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmedLine = line.trimStart();
|
const trimmedLine = line.trimStart();
|
||||||
if (before === "- ") {
|
if (before === "- ") {
|
||||||
// Для маркированного списка проверяем различные варианты
|
// Для маркированного списка проверяем различные варианты
|
||||||
if (!trimmedLine.match(/^[-*+]\s/)) {
|
if (trimmedLine.match(/^[-*+]\s/)) {
|
||||||
|
hasAnyMarker = true;
|
||||||
|
} else {
|
||||||
allLinesHaveMarker = false;
|
allLinesHaveMarker = false;
|
||||||
}
|
}
|
||||||
} else if (before === "1. ") {
|
} else if (before === "1. ") {
|
||||||
// Для нумерованного списка
|
// Для нумерованного списка
|
||||||
if (!trimmedLine.match(/^\d+\.\s/)) {
|
if (trimmedLine.match(/^\d+\.\s/)) {
|
||||||
|
hasAnyMarker = true;
|
||||||
|
} else {
|
||||||
allLinesHaveMarker = false;
|
allLinesHaveMarker = false;
|
||||||
}
|
}
|
||||||
} else if (before === "- [ ] ") {
|
} else if (before === "- [ ] ") {
|
||||||
// Для чекбокса
|
// Для чекбокса
|
||||||
if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||||
|
hasAnyMarker = true;
|
||||||
|
} else {
|
||||||
allLinesHaveMarker = false;
|
allLinesHaveMarker = false;
|
||||||
}
|
}
|
||||||
} else if (before === "> ") {
|
} else if (before === "> ") {
|
||||||
// Для цитаты
|
// Для цитаты
|
||||||
if (!trimmedLine.startsWith("> ")) {
|
if (trimmedLine.startsWith("> ")) {
|
||||||
|
hasAnyMarker = true;
|
||||||
|
} else {
|
||||||
allLinesHaveMarker = false;
|
allLinesHaveMarker = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -520,12 +530,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 +668,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;
|
||||||
|
|
||||||
// Более точный расчет ширины символа
|
// Более точный расчет ширины символа
|
||||||
// Создаем временный элемент для измерения
|
// Создаем временный элемент для измерения
|
||||||
|
|||||||
@ -31,7 +31,7 @@ interface NoteItemProps {
|
|||||||
|
|
||||||
export const NoteItem: React.FC<NoteItemProps> = ({
|
export const NoteItem: React.FC<NoteItemProps> = ({
|
||||||
note,
|
note,
|
||||||
onDelete: _onDelete,
|
onDelete,
|
||||||
onPin,
|
onPin,
|
||||||
onArchive,
|
onArchive,
|
||||||
onReload,
|
onReload,
|
||||||
@ -41,8 +41,8 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
const [showArchiveModal, setShowArchiveModal] = useState(false);
|
const [showArchiveModal, setShowArchiveModal] = useState(false);
|
||||||
const [editImages, setEditImages] = useState<File[]>([]);
|
const [editImages, setEditImages] = useState<File[]>([]);
|
||||||
const [editFiles, setEditFiles] = useState<File[]>([]);
|
const [editFiles, setEditFiles] = useState<File[]>([]);
|
||||||
const [deletedImageIds, setDeletedImageIds] = useState<(number | string)[]>([]);
|
const [deletedImageIds, setDeletedImageIds] = useState<number[]>([]);
|
||||||
const [deletedFileIds, setDeletedFileIds] = useState<(number | string)[]>([]);
|
const [deletedFileIds, setDeletedFileIds] = useState<number[]>([]);
|
||||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||||
const [toolbarPosition, setToolbarPosition] = useState({ top: 0, left: 0 });
|
const [toolbarPosition, setToolbarPosition] = useState({ top: 0, left: 0 });
|
||||||
@ -140,19 +140,19 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
setLocalPreviewMode(false);
|
setLocalPreviewMode(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteExistingImage = (imageId: number | string) => {
|
const handleDeleteExistingImage = (imageId: number) => {
|
||||||
setDeletedImageIds([...deletedImageIds, imageId]);
|
setDeletedImageIds([...deletedImageIds, imageId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteExistingFile = (fileId: number | string) => {
|
const handleDeleteExistingFile = (fileId: number) => {
|
||||||
setDeletedFileIds([...deletedFileIds, fileId]);
|
setDeletedFileIds([...deletedFileIds, fileId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRestoreImage = (imageId: number | string) => {
|
const handleRestoreImage = (imageId: number) => {
|
||||||
setDeletedImageIds(deletedImageIds.filter((id) => id !== imageId));
|
setDeletedImageIds(deletedImageIds.filter((id) => id !== imageId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRestoreFile = (fileId: number | string) => {
|
const handleRestoreFile = (fileId: number) => {
|
||||||
setDeletedFileIds(deletedFileIds.filter((id) => id !== fileId));
|
setDeletedFileIds(deletedFileIds.filter((id) => id !== fileId));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -337,27 +337,36 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
|
|
||||||
// Определяем, есть ли уже такие маркеры на всех строках
|
// Определяем, есть ли уже такие маркеры на всех строках
|
||||||
let allLinesHaveMarker = true;
|
let allLinesHaveMarker = true;
|
||||||
|
let hasAnyMarker = false;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmedLine = line.trimStart();
|
const trimmedLine = line.trimStart();
|
||||||
if (before === "- ") {
|
if (before === "- ") {
|
||||||
// Для маркированного списка проверяем различные варианты
|
// Для маркированного списка проверяем различные варианты
|
||||||
if (!trimmedLine.match(/^[-*+]\s/)) {
|
if (trimmedLine.match(/^[-*+]\s/)) {
|
||||||
|
hasAnyMarker = true;
|
||||||
|
} else {
|
||||||
allLinesHaveMarker = false;
|
allLinesHaveMarker = false;
|
||||||
}
|
}
|
||||||
} else if (before === "1. ") {
|
} else if (before === "1. ") {
|
||||||
// Для нумерованного списка
|
// Для нумерованного списка
|
||||||
if (!trimmedLine.match(/^\d+\.\s/)) {
|
if (trimmedLine.match(/^\d+\.\s/)) {
|
||||||
|
hasAnyMarker = true;
|
||||||
|
} else {
|
||||||
allLinesHaveMarker = false;
|
allLinesHaveMarker = false;
|
||||||
}
|
}
|
||||||
} else if (before === "- [ ] ") {
|
} else if (before === "- [ ] ") {
|
||||||
// Для чекбокса
|
// Для чекбокса
|
||||||
if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||||
|
hasAnyMarker = true;
|
||||||
|
} else {
|
||||||
allLinesHaveMarker = false;
|
allLinesHaveMarker = false;
|
||||||
}
|
}
|
||||||
} else if (before === "> ") {
|
} else if (before === "> ") {
|
||||||
// Для цитаты
|
// Для цитаты
|
||||||
if (!trimmedLine.startsWith("> ")) {
|
if (trimmedLine.startsWith("> ")) {
|
||||||
|
hasAnyMarker = true;
|
||||||
|
} else {
|
||||||
allLinesHaveMarker = false;
|
allLinesHaveMarker = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -619,6 +628,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;
|
||||||
|
|
||||||
// Более точный расчет ширины символа
|
// Более точный расчет ширины символа
|
||||||
// Создаем временный элемент для измерения
|
// Создаем временный элемент для измерения
|
||||||
@ -741,12 +751,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;
|
||||||
}
|
}
|
||||||
@ -1374,6 +1386,11 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
{note.files
|
{note.files
|
||||||
.filter((file) => !deletedFileIds.includes(file.id))
|
.filter((file) => !deletedFileIds.includes(file.id))
|
||||||
.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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 для поиска
|
||||||
|
|||||||
@ -10,8 +10,7 @@ export const useNotification = () => {
|
|||||||
message: string,
|
message: string,
|
||||||
type: "info" | "success" | "error" | "warning" = "info"
|
type: "info" | "success" | "error" | "warning" = "info"
|
||||||
) => {
|
) => {
|
||||||
const id = `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
const id = dispatch(addNotification({ message, type })).payload.id;
|
||||||
dispatch(addNotification({ id, message, type }));
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
dispatch(removeNotification(id));
|
dispatch(removeNotification(id));
|
||||||
}, 4000);
|
}, 4000);
|
||||||
|
|||||||
91
src/main.tsx
91
src/main.tsx
@ -14,7 +14,7 @@ import { addNotification } from "./store/slices/uiSlice";
|
|||||||
|
|
||||||
// Регистрация PWA (vite-plugin-pwa автоматически внедряет регистрацию через injectRegister: "auto")
|
// Регистрация PWA (vite-plugin-pwa автоматически внедряет регистрацию через injectRegister: "auto")
|
||||||
|
|
||||||
// Инициализация offline функционала (неблокирующая)
|
// Инициализация offline функционала
|
||||||
async function initOfflineMode() {
|
async function initOfflineMode() {
|
||||||
try {
|
try {
|
||||||
console.log('[Init] Initializing offline mode...');
|
console.log('[Init] Initializing offline mode...');
|
||||||
@ -23,46 +23,28 @@ async function initOfflineMode() {
|
|||||||
await dbManager.init();
|
await dbManager.init();
|
||||||
console.log('[Init] IndexedDB initialized');
|
console.log('[Init] IndexedDB initialized');
|
||||||
|
|
||||||
// Устанавливаем начальное состояние на основе navigator.onLine (мгновенно, без блокировки)
|
// Проверка состояния сети
|
||||||
const initialOnlineState = navigator.onLine;
|
const isOnline = await checkNetworkStatus();
|
||||||
store.dispatch(setOfflineMode(!initialOnlineState));
|
store.dispatch(setOfflineMode(!isOnline));
|
||||||
console.log(`[Init] Initial network status (navigator.onLine): ${initialOnlineState ? 'online' : 'offline'}`);
|
console.log(`[Init] Network status: ${isOnline ? 'online' : 'offline'}`);
|
||||||
|
|
||||||
// Проверка состояния сети в фоне (не блокирует загрузку приложения)
|
|
||||||
checkNetworkStatus().then((isOnline) => {
|
|
||||||
store.dispatch(setOfflineMode(!isOnline));
|
|
||||||
console.log(`[Init] Network status (after check): ${isOnline ? 'online' : 'offline'}`);
|
|
||||||
}).catch((error) => {
|
|
||||||
console.warn('[Init] Network check failed, using navigator.onLine:', error);
|
|
||||||
// В случае ошибки используем navigator.onLine
|
|
||||||
store.dispatch(setOfflineMode(!navigator.onLine));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Установка listeners для событий сети
|
// Установка listeners для событий сети
|
||||||
networkListener.onOnline(async () => {
|
networkListener.onOnline(async () => {
|
||||||
console.log('[Network] Online event detected');
|
console.log('[Network] Online event detected');
|
||||||
// Небольшая задержка перед проверкой для стабилизации соединения
|
const isOnline = await checkNetworkStatus();
|
||||||
setTimeout(async () => {
|
store.dispatch(setOfflineMode(!isOnline));
|
||||||
try {
|
|
||||||
const isOnline = await checkNetworkStatus();
|
|
||||||
store.dispatch(setOfflineMode(!isOnline));
|
|
||||||
|
|
||||||
if (isOnline) {
|
if (isOnline) {
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
addNotification({
|
addNotification({
|
||||||
message: 'Подключение восстановлено, начинаем синхронизацию...',
|
message: 'Подключение восстановлено, начинаем синхронизацию...',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Запуск синхронизации
|
// Запуск синхронизации
|
||||||
await syncService.startSync();
|
await syncService.startSync();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('[Network] Error checking network status:', error);
|
|
||||||
store.dispatch(setOfflineMode(!navigator.onLine));
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
networkListener.onOffline(() => {
|
networkListener.onOffline(() => {
|
||||||
@ -77,40 +59,29 @@ async function initOfflineMode() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Обновление счетчика ожидающих синхронизацию
|
// Обновление счетчика ожидающих синхронизацию
|
||||||
dbManager.getPendingSyncCount().then((pendingCount) => {
|
const pendingCount = await dbManager.getPendingSyncCount();
|
||||||
store.dispatch(setPendingSyncCount(pendingCount));
|
store.dispatch(setPendingSyncCount(pendingCount));
|
||||||
|
|
||||||
if (pendingCount > 0) {
|
if (pendingCount > 0) {
|
||||||
console.log(`[Init] Found ${pendingCount} pending sync items`);
|
console.log(`[Init] Found ${pendingCount} pending sync items`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Автоматическая синхронизация при старте если есть что синхронизировать
|
// Автоматическая синхронизация при старте если есть что синхронизировать
|
||||||
// Проверяем статус сети перед синхронизацией
|
if (isOnline && pendingCount > 0) {
|
||||||
checkNetworkStatus().then((isOnline) => {
|
console.log('[Init] Starting initial sync...');
|
||||||
if (isOnline && pendingCount > 0) {
|
// Небольшая задержка для инициализации UI
|
||||||
console.log('[Init] Starting initial sync...');
|
setTimeout(() => {
|
||||||
// Небольшая задержка для инициализации UI
|
syncService.startSync();
|
||||||
setTimeout(() => {
|
}, 2000);
|
||||||
syncService.startSync();
|
}
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
// Если проверка сети не удалась, не запускаем синхронизацию
|
|
||||||
console.log('[Init] Skipping initial sync due to network check failure');
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error('[Init] Error getting pending sync count:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[Init] Offline mode initialized successfully');
|
console.log('[Init] Offline mode initialized successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Init] Error initializing offline mode:', error);
|
console.error('[Init] Error initializing offline mode:', error);
|
||||||
// Не блокируем запуск приложения даже при ошибке
|
|
||||||
store.dispatch(setOfflineMode(!navigator.onLine));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запуск инициализации (не блокирует рендеринг React)
|
// Запуск инициализации
|
||||||
initOfflineMode();
|
initOfflineMode();
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
|||||||
@ -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("");
|
||||||
|
|||||||
@ -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";
|
||||||
@ -25,6 +26,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const user = useAppSelector((state) => state.profile.user);
|
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,8 +164,8 @@ const SettingsPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await userApi.updateProfile({
|
await userApi.updateProfile({
|
||||||
accent_color: selectedAccentColor,
|
accent_color: selectedAccentColor,
|
||||||
show_edit_date: showEditDate ? 1 : 0,
|
show_edit_date: showEditDate,
|
||||||
colored_icons: coloredIcons ? 1 : 0,
|
colored_icons: coloredIcons,
|
||||||
});
|
});
|
||||||
dispatch(setAccentColorAction(selectedAccentColor));
|
dispatch(setAccentColorAction(selectedAccentColor));
|
||||||
setAccentColor(selectedAccentColor);
|
setAccentColor(selectedAccentColor);
|
||||||
@ -271,7 +273,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRestoreNote = async (id: number | string) => {
|
const handleRestoreNote = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await notesApi.unarchive(id);
|
await notesApi.unarchive(id);
|
||||||
await loadArchivedNotes();
|
await loadArchivedNotes();
|
||||||
@ -285,7 +287,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletePermanent = async (id: number | string) => {
|
const handleDeletePermanent = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await notesApi.deleteArchived(id);
|
await notesApi.deleteArchived(id);
|
||||||
await loadArchivedNotes();
|
await loadArchivedNotes();
|
||||||
@ -418,8 +420,8 @@ const SettingsPage: React.FC = () => {
|
|||||||
|
|
||||||
// Загружаем версию из IndexedDB
|
// Загружаем версию из IndexedDB
|
||||||
try {
|
try {
|
||||||
const userId = (user as any)?.id;
|
const userId = user?.id;
|
||||||
const localVer = userId && typeof userId === 'number'
|
const localVer = userId
|
||||||
? await dbManager.getDataVersionByUserId(userId)
|
? await dbManager.getDataVersionByUserId(userId)
|
||||||
: await dbManager.getDataVersion();
|
: await dbManager.getDataVersion();
|
||||||
setIndexedDBVersion(localVer);
|
setIndexedDBVersion(localVer);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -9,7 +9,7 @@ interface AuthState {
|
|||||||
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
isAuthenticated: localStorage.getItem("isAuthenticated") === "true",
|
isAuthenticated: localStorage.getItem("isAuthenticated") === "true",
|
||||||
userId: localStorage.getItem("userId") ? parseInt(localStorage.getItem("userId")!, 10) : null,
|
userId: null,
|
||||||
username: localStorage.getItem("username") || null,
|
username: localStorage.getItem("username") || null,
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
@ -26,7 +26,6 @@ const authSlice = createSlice({
|
|||||||
state.userId = action.payload.userId;
|
state.userId = action.payload.userId;
|
||||||
state.username = action.payload.username;
|
state.username = action.payload.username;
|
||||||
localStorage.setItem("isAuthenticated", "true");
|
localStorage.setItem("isAuthenticated", "true");
|
||||||
localStorage.setItem("userId", action.payload.userId.toString());
|
|
||||||
localStorage.setItem("username", action.payload.username);
|
localStorage.setItem("username", action.payload.username);
|
||||||
},
|
},
|
||||||
clearAuth: (state) => {
|
clearAuth: (state) => {
|
||||||
@ -34,7 +33,6 @@ const authSlice = createSlice({
|
|||||||
state.userId = null;
|
state.userId = null;
|
||||||
state.username = null;
|
state.username = null;
|
||||||
localStorage.removeItem("isAuthenticated");
|
localStorage.removeItem("isAuthenticated");
|
||||||
localStorage.removeItem("userId");
|
|
||||||
localStorage.removeItem("username");
|
localStorage.removeItem("username");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -50,11 +50,9 @@ const uiSlice = createSlice({
|
|||||||
},
|
},
|
||||||
addNotification: (
|
addNotification: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<Omit<Notification, "id"> | Notification>
|
action: PayloadAction<Omit<Notification, "id">>
|
||||||
) => {
|
) => {
|
||||||
const id = ('id' in action.payload && action.payload.id)
|
const id = `notification-${Date.now()}-${Math.random()
|
||||||
? action.payload.id
|
|
||||||
: `notification-${Date.now()}-${Math.random()
|
|
||||||
.toString(36)
|
.toString(36)
|
||||||
.substr(2, 9)}`;
|
.substr(2, 9)}`;
|
||||||
state.notifications.push({ ...action.payload, id });
|
state.notifications.push({ ...action.payload, id });
|
||||||
|
|||||||
@ -118,6 +118,7 @@ body {
|
|||||||
button {
|
button {
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
-moz-tap-highlight-color: transparent;
|
-moz-tap-highlight-color: transparent;
|
||||||
|
tap-highlight-color: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
@ -136,6 +137,7 @@ a,
|
|||||||
div[role="button"] {
|
div[role="button"] {
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
-moz-tap-highlight-color: transparent;
|
-moz-tap-highlight-color: transparent;
|
||||||
|
tap-highlight-color: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,8 @@
|
|||||||
*/
|
*/
|
||||||
export function getImageUrl(
|
export function getImageUrl(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
noteId: number | string,
|
noteId: number,
|
||||||
imageId: number | string
|
imageId: number
|
||||||
): string {
|
): string {
|
||||||
// Если путь уже является полным URL (начинается с http:// или https://)
|
// Если путь уже является полным URL (начинается с http:// или https://)
|
||||||
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
||||||
@ -47,8 +47,8 @@ export function getImageUrl(
|
|||||||
*/
|
*/
|
||||||
export function getFileUrl(
|
export function getFileUrl(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
noteId: number | string,
|
noteId: number,
|
||||||
fileId: number | string
|
fileId: number
|
||||||
): string {
|
): string {
|
||||||
// Если путь уже является полным URL (начинается с http:// или https://)
|
// Если путь уже является полным URL (начинается с http:// или https://)
|
||||||
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
||||||
|
|||||||
@ -24,113 +24,50 @@ const spoilerExtension = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Кастомный renderer для внешних ссылок и чекбоксов
|
// Кастомный renderer для внешних ссылок и чекбоксов
|
||||||
const renderer = new marked.Renderer();
|
const renderer: any = {
|
||||||
|
link(token: any) {
|
||||||
|
const href = token.href;
|
||||||
|
const title = token.title;
|
||||||
|
const text = token.text;
|
||||||
|
|
||||||
// Переопределяем link для внешних ссылок
|
|
||||||
const originalLink = renderer.link.bind(renderer);
|
|
||||||
renderer.link = function(token: any) {
|
|
||||||
const href = token.href;
|
|
||||||
const title = token.title;
|
|
||||||
const text = token.text;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = new URL(href, window.location.href);
|
|
||||||
const isExternal = url.origin !== window.location.origin;
|
|
||||||
|
|
||||||
if (isExternal) {
|
|
||||||
return `<a href="${href}" title="${
|
|
||||||
title || ""
|
|
||||||
}" target="_blank" rel="noopener noreferrer" class="external-link">${text}</a>`;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return originalLink(token);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Переопределяем listitem для поддержки чекбоксов
|
|
||||||
renderer.listitem = function(token: any) {
|
|
||||||
const task = token.task;
|
|
||||||
const checked = token.checked;
|
|
||||||
|
|
||||||
// Получаем токены для обработки
|
|
||||||
const tokens = token.tokens || [];
|
|
||||||
let text: string;
|
|
||||||
|
|
||||||
// Блоковые типы токенов, которые нельзя обрабатывать через parseInline
|
|
||||||
const blockTypes = ['list', 'blockquote', 'code', 'heading', 'paragraph', 'hr', 'table'];
|
|
||||||
|
|
||||||
// Обрабатываем токены вручную, избегая parseInline для блоковых элементов
|
|
||||||
if (tokens.length > 0) {
|
|
||||||
try {
|
try {
|
||||||
// Разделяем токены на inline и блоковые
|
const url = new URL(href, window.location.href);
|
||||||
const inlineTokens: any[] = [];
|
const isExternal = url.origin !== window.location.origin;
|
||||||
const blockTokens: any[] = [];
|
|
||||||
|
|
||||||
tokens.forEach((t: any) => {
|
if (isExternal) {
|
||||||
if (blockTypes.includes(t.type)) {
|
return `<a href="${href}" title="${
|
||||||
blockTokens.push(t);
|
title || ""
|
||||||
} else {
|
}" target="_blank" rel="noopener noreferrer" class="external-link">${text}</a>`;
|
||||||
inlineTokens.push(t);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обрабатываем inline токены только если они есть
|
|
||||||
let inlineText = '';
|
|
||||||
if (inlineTokens.length > 0) {
|
|
||||||
try {
|
|
||||||
inlineText = this.parser.parseInline(inlineTokens);
|
|
||||||
} catch (inlineError) {
|
|
||||||
// Если ошибка при обработке inline, просто игнорируем их
|
|
||||||
console.warn('Error parsing inline tokens in listitem:', inlineError);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
// Обрабатываем блоковые токены через parser
|
return `<a href="${href}"${title ? ` title="${title}"` : ""}>${text}</a>`;
|
||||||
let blockText = '';
|
},
|
||||||
if (blockTokens.length > 0) {
|
// Кастомный renderer для элементов списка с чекбоксами
|
||||||
try {
|
listitem(token: any) {
|
||||||
blockText = this.parser.parse(blockTokens);
|
const task = token.task;
|
||||||
} catch (blockError) {
|
const checked = token.checked;
|
||||||
// Если ошибка при обработке блоков, обрабатываем через стандартный renderer
|
|
||||||
console.warn('Error parsing block tokens in listitem:', blockError);
|
|
||||||
// Пытаемся обработать каждый блок отдельно
|
|
||||||
blockText = blockTokens.map((bt: any) => {
|
|
||||||
try {
|
|
||||||
return this.parser.parse([bt]);
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
text = inlineText + blockText;
|
// Используем tokens для правильной обработки форматирования внутри элементов списка
|
||||||
|
// token.tokens содержит массив токенов для вложенного содержимого
|
||||||
|
const tokens = token.tokens || [];
|
||||||
|
let text: string;
|
||||||
|
|
||||||
// Если после обработки текст пустой, используем fallback
|
if (tokens.length > 0) {
|
||||||
if (!text || text.trim() === '') {
|
// Используем this.parser.parseInline для правильной обработки вложенного форматирования
|
||||||
text = token.text || '';
|
// this указывает на экземпляр Parser в контексте renderer
|
||||||
}
|
text = this.parser.parseInline(tokens);
|
||||||
} catch (error) {
|
} else {
|
||||||
// Если общая ошибка, используем fallback - обрабатываем через стандартный parser
|
// Fallback на token.text, если tokens отсутствуют
|
||||||
try {
|
text = token.text || '';
|
||||||
text = this.parser.parse(tokens);
|
|
||||||
} catch (parseError) {
|
|
||||||
// Последний fallback - используем raw text
|
|
||||||
console.warn('Error parsing list item tokens:', parseError);
|
|
||||||
text = token.text || token.raw || '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
text = token.text || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если это задача (чекбокс), добавляем чекбокс
|
if (task) {
|
||||||
if (task) {
|
const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} />`;
|
||||||
const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} disabled />`;
|
return `<li class="task-list-item">${checkbox} ${text}</li>\n`;
|
||||||
return `<li class="task-list-item">${checkbox} ${text}</li>\n`;
|
}
|
||||||
}
|
return `<li>${text}</li>\n`;
|
||||||
|
},
|
||||||
return `<li>${text}</li>\n`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Настройка marked
|
// Настройка marked
|
||||||
|
|||||||
@ -39,8 +39,6 @@ export function waitForIndexedDB(): Promise<IDBDatabase> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверка состояния сети (более надежный метод)
|
* Проверка состояния сети (более надежный метод)
|
||||||
* Не блокирует выполнение, всегда возвращает результат
|
|
||||||
* Различает "нет интернета" (offline) и "не авторизован" (401)
|
|
||||||
*/
|
*/
|
||||||
export async function checkNetworkStatus(): Promise<boolean> {
|
export async function checkNetworkStatus(): Promise<boolean> {
|
||||||
// Простая проверка navigator.onLine
|
// Простая проверка navigator.onLine
|
||||||
@ -51,11 +49,10 @@ export async function checkNetworkStatus(): Promise<boolean> {
|
|||||||
// Дополнительная проверка через fetch с коротким таймаутом
|
// Дополнительная проверка через fetch с коротким таймаутом
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
// Уменьшаем таймаут для более быстрого ответа
|
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
|
||||||
|
|
||||||
// Используем /auth/status так как он всегда доступен при наличии сети
|
// Используем /auth/status так как он всегда доступен при наличии сети
|
||||||
await fetch('/api/auth/status', {
|
const response = await fetch('/api/auth/status', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
cache: 'no-cache',
|
cache: 'no-cache',
|
||||||
@ -63,27 +60,10 @@ export async function checkNetworkStatus(): Promise<boolean> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
return response.ok;
|
||||||
// Если получили ответ (даже 401), значит интернет есть
|
|
||||||
// 401 означает "не авторизован", но НЕ "нет интернета"
|
|
||||||
// Любой ответ (даже ошибка) означает, что сеть работает
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Если запрос не удался из-за таймаута или сетевой ошибки
|
// Если запрос не удался, но navigator.onLine = true, считаем что онлайн
|
||||||
// (AbortError, NetworkError, TypeError и т.д.)
|
// (возможно, просто таймаут или другая проблема)
|
||||||
// Это означает, что интернета действительно нет
|
|
||||||
if (error instanceof Error) {
|
|
||||||
// AbortError означает таймаут - нет интернета
|
|
||||||
if (error.name === 'AbortError') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// TypeError обычно означает, что запрос не может быть выполнен
|
|
||||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// В остальных случаях считаем, что интернет есть
|
|
||||||
// (возможно, просто ошибка авторизации или другая проблема)
|
|
||||||
return navigator.onLine;
|
return navigator.onLine;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,10 +12,7 @@ export default defineConfig({
|
|||||||
"icon.svg",
|
"icon.svg",
|
||||||
"icons/icon-192x192.png",
|
"icons/icon-192x192.png",
|
||||||
"icons/icon-512x512.png",
|
"icons/icon-512x512.png",
|
||||||
"manifest.json",
|
|
||||||
],
|
],
|
||||||
// Включаем стратегию для offline работы
|
|
||||||
strategies: "generateSW",
|
|
||||||
manifest: {
|
manifest: {
|
||||||
name: "NoteJS - Система заметок",
|
name: "NoteJS - Система заметок",
|
||||||
short_name: "NoteJS",
|
short_name: "NoteJS",
|
||||||
@ -91,41 +88,8 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
// Кэшируем все статические ресурсы для offline работы
|
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot}"],
|
||||||
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot,json}"],
|
|
||||||
// Стратегия для главной страницы - CacheFirst для offline работы
|
|
||||||
navigateFallback: "/index.html",
|
|
||||||
navigateFallbackDenylist: [/^\/api/, /^\/_/],
|
|
||||||
// Кэширование для offline работы приложения
|
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
|
||||||
// Стратегия для корневого пути и index.html
|
|
||||||
urlPattern: ({ request }) =>
|
|
||||||
request.destination === 'document' ||
|
|
||||||
request.url.endsWith('/') ||
|
|
||||||
request.url.endsWith('/index.html'),
|
|
||||||
handler: "NetworkFirst",
|
|
||||||
options: {
|
|
||||||
cacheName: "html-cache",
|
|
||||||
expiration: {
|
|
||||||
maxEntries: 10,
|
|
||||||
maxAgeSeconds: 24 * 60 * 60, // 24 hours
|
|
||||||
},
|
|
||||||
networkTimeoutSeconds: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Статические ресурсы (JS, CSS) - кэшируем для offline
|
|
||||||
urlPattern: /\.(?:js|css|woff|woff2|ttf|eot)$/,
|
|
||||||
handler: "CacheFirst",
|
|
||||||
options: {
|
|
||||||
cacheName: "static-resources-cache",
|
|
||||||
expiration: {
|
|
||||||
maxEntries: 200,
|
|
||||||
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
urlPattern: /^https:\/\/api\./,
|
urlPattern: /^https:\/\/api\./,
|
||||||
handler: "NetworkFirst",
|
handler: "NetworkFirst",
|
||||||
@ -135,7 +99,6 @@ export default defineConfig({
|
|||||||
maxEntries: 50,
|
maxEntries: 50,
|
||||||
maxAgeSeconds: 60 * 60, // 1 hour
|
maxAgeSeconds: 60 * 60, // 1 hour
|
||||||
},
|
},
|
||||||
networkTimeoutSeconds: 3,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -147,7 +110,7 @@ export default defineConfig({
|
|||||||
maxEntries: 100,
|
maxEntries: 100,
|
||||||
maxAgeSeconds: 24 * 60 * 60, // 24 hours
|
maxAgeSeconds: 24 * 60 * 60, // 24 hours
|
||||||
},
|
},
|
||||||
networkTimeoutSeconds: 3,
|
networkTimeoutSeconds: 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user