Compare commits
2 Commits
a5f4e87056
...
2ec93b8cc2
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ec93b8cc2 | |||
| 3c2b23c699 |
@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.nsn25edhihg"
|
||||
"revision": "0.o51qplqi6t"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
@ -34,22 +34,31 @@ axiosClient.interceptors.response.use(
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
// Список URL, где 401 означает неправильный пароль, а не истечение сессии
|
||||
// или где не нужно делать редирект (например, проверка статуса)
|
||||
const passwordProtectedUrls = [
|
||||
"/login", // Страница входа
|
||||
"/register", // Страница регистрации
|
||||
"/notes/archived/all", // Удаление всех архивных заметок
|
||||
"/user/delete-account", // Удаление аккаунта
|
||||
"/auth/status", // Проверка статуса (не нужно делать редирект при ошибке)
|
||||
];
|
||||
|
||||
// Проверяем, является ли это запросом с проверкой пароля
|
||||
// Проверяем, является ли это запросом с проверкой пароля или статуса
|
||||
const isPasswordProtected = passwordProtectedUrls.some((url) =>
|
||||
error.config?.url?.includes(url)
|
||||
);
|
||||
|
||||
// Разлогиниваем только если это НЕ запрос с проверкой пароля
|
||||
if (!isPasswordProtected) {
|
||||
// Разлогиниваем только если это НЕ запрос с проверкой пароля/статуса
|
||||
// и только если есть ответ от сервера (не offline режим)
|
||||
if (!isPasswordProtected && error.response) {
|
||||
localStorage.removeItem("isAuthenticated");
|
||||
window.location.href = "/";
|
||||
localStorage.removeItem("userId");
|
||||
localStorage.removeItem("username");
|
||||
// Не делаем редирект, если нет интернета (error.response будет undefined)
|
||||
// ProtectedRoute сам обработает это
|
||||
if (navigator.onLine) {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { useAppSelector, useAppDispatch } from "../store/hooks";
|
||||
import { setAuth, clearAuth } from "../store/slices/authSlice";
|
||||
import { authApi } from "../api/authApi";
|
||||
import { LoadingOverlay } from "./common/LoadingOverlay";
|
||||
import { checkNetworkStatus } from "../utils/offlineManager";
|
||||
|
||||
export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
@ -15,26 +16,84 @@ export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const authStatus = await authApi.checkStatus();
|
||||
if (authStatus.authenticated) {
|
||||
// Проверяем, есть ли интернет
|
||||
const isOnline = await checkNetworkStatus();
|
||||
|
||||
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(
|
||||
setAuth({
|
||||
userId: authStatus.userId!,
|
||||
username: authStatus.username!,
|
||||
userId: parseInt(savedUserId, 10),
|
||||
username: savedUsername,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(clearAuth());
|
||||
}
|
||||
} catch {
|
||||
dispatch(clearAuth());
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Всегда проверяем статус аутентификации при монтировании,
|
||||
// независимо от начального состояния Redux (localStorage может быть устаревшим)
|
||||
checkAuth();
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
@ -296,6 +296,7 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
||||
|
||||
{hasSelection && (
|
||||
<>
|
||||
<div className="floating-toolbar-separator" />
|
||||
<button
|
||||
className="floating-toolbar-btn"
|
||||
onClick={handleCopy}
|
||||
@ -367,22 +368,6 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
|
||||
>
|
||||
<Icon icon="mdi:eye-off" />
|
||||
</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>
|
||||
|
||||
@ -239,6 +239,128 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
const end = textarea.selectionEnd;
|
||||
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;
|
||||
let hasAnyMarker = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trimStart();
|
||||
if (before === "- ") {
|
||||
// Для маркированного списка проверяем различные варианты
|
||||
if (trimmedLine.match(/^[-*+]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "1. ") {
|
||||
// Для нумерованного списка
|
||||
if (trimmedLine.match(/^\d+\.\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "- [ ] ") {
|
||||
// Для чекбокса
|
||||
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "> ") {
|
||||
// Для цитаты
|
||||
if (trimmedLine.startsWith("> ")) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
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;
|
||||
|
||||
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
||||
|
||||
@ -325,6 +325,128 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const end = textarea.selectionEnd;
|
||||
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;
|
||||
let hasAnyMarker = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trimStart();
|
||||
if (before === "- ") {
|
||||
// Для маркированного списка проверяем различные варианты
|
||||
if (trimmedLine.match(/^[-*+]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "1. ") {
|
||||
// Для нумерованного списка
|
||||
if (trimmedLine.match(/^\d+\.\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "- [ ] ") {
|
||||
// Для чекбокса
|
||||
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "> ") {
|
||||
// Для цитаты
|
||||
if (trimmedLine.startsWith("> ")) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
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;
|
||||
|
||||
// Проверяем область вокруг выделения (расширяем для проверки тегов)
|
||||
|
||||
97
src/main.tsx
97
src/main.tsx
@ -14,7 +14,7 @@ import { addNotification } from "./store/slices/uiSlice";
|
||||
|
||||
// Регистрация PWA (vite-plugin-pwa автоматически внедряет регистрацию через injectRegister: "auto")
|
||||
|
||||
// Инициализация offline функционала
|
||||
// Инициализация offline функционала (неблокирующая)
|
||||
async function initOfflineMode() {
|
||||
try {
|
||||
console.log('[Init] Initializing offline mode...');
|
||||
@ -23,28 +23,46 @@ async function initOfflineMode() {
|
||||
await dbManager.init();
|
||||
console.log('[Init] IndexedDB initialized');
|
||||
|
||||
// Проверка состояния сети
|
||||
const isOnline = await checkNetworkStatus();
|
||||
store.dispatch(setOfflineMode(!isOnline));
|
||||
console.log(`[Init] Network status: ${isOnline ? 'online' : 'offline'}`);
|
||||
// Устанавливаем начальное состояние на основе navigator.onLine (мгновенно, без блокировки)
|
||||
const initialOnlineState = navigator.onLine;
|
||||
store.dispatch(setOfflineMode(!initialOnlineState));
|
||||
console.log(`[Init] Initial network status (navigator.onLine): ${initialOnlineState ? '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 для событий сети
|
||||
networkListener.onOnline(async () => {
|
||||
console.log('[Network] Online event detected');
|
||||
const isOnline = await checkNetworkStatus();
|
||||
store.dispatch(setOfflineMode(!isOnline));
|
||||
|
||||
if (isOnline) {
|
||||
store.dispatch(
|
||||
addNotification({
|
||||
message: 'Подключение восстановлено, начинаем синхронизацию...',
|
||||
type: 'info',
|
||||
})
|
||||
);
|
||||
|
||||
// Запуск синхронизации
|
||||
await syncService.startSync();
|
||||
}
|
||||
// Небольшая задержка перед проверкой для стабилизации соединения
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const isOnline = await checkNetworkStatus();
|
||||
store.dispatch(setOfflineMode(!isOnline));
|
||||
|
||||
if (isOnline) {
|
||||
store.dispatch(
|
||||
addNotification({
|
||||
message: 'Подключение восстановлено, начинаем синхронизацию...',
|
||||
type: 'info',
|
||||
})
|
||||
);
|
||||
|
||||
// Запуск синхронизации
|
||||
await syncService.startSync();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Network] Error checking network status:', error);
|
||||
store.dispatch(setOfflineMode(!navigator.onLine));
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
networkListener.onOffline(() => {
|
||||
@ -59,29 +77,40 @@ async function initOfflineMode() {
|
||||
});
|
||||
|
||||
// Обновление счетчика ожидающих синхронизацию
|
||||
const pendingCount = await dbManager.getPendingSyncCount();
|
||||
store.dispatch(setPendingSyncCount(pendingCount));
|
||||
|
||||
if (pendingCount > 0) {
|
||||
console.log(`[Init] Found ${pendingCount} pending sync items`);
|
||||
}
|
||||
dbManager.getPendingSyncCount().then((pendingCount) => {
|
||||
store.dispatch(setPendingSyncCount(pendingCount));
|
||||
|
||||
if (pendingCount > 0) {
|
||||
console.log(`[Init] Found ${pendingCount} pending sync items`);
|
||||
}
|
||||
|
||||
// Автоматическая синхронизация при старте если есть что синхронизировать
|
||||
if (isOnline && pendingCount > 0) {
|
||||
console.log('[Init] Starting initial sync...');
|
||||
// Небольшая задержка для инициализации UI
|
||||
setTimeout(() => {
|
||||
syncService.startSync();
|
||||
}, 2000);
|
||||
}
|
||||
// Автоматическая синхронизация при старте если есть что синхронизировать
|
||||
// Проверяем статус сети перед синхронизацией
|
||||
checkNetworkStatus().then((isOnline) => {
|
||||
if (isOnline && pendingCount > 0) {
|
||||
console.log('[Init] Starting initial sync...');
|
||||
// Небольшая задержка для инициализации UI
|
||||
setTimeout(() => {
|
||||
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');
|
||||
} catch (error) {
|
||||
console.error('[Init] Error initializing offline mode:', error);
|
||||
// Не блокируем запуск приложения даже при ошибке
|
||||
store.dispatch(setOfflineMode(!navigator.onLine));
|
||||
}
|
||||
}
|
||||
|
||||
// Запуск инициализации
|
||||
// Запуск инициализации (не блокирует рендеринг React)
|
||||
initOfflineMode();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
|
||||
@ -9,7 +9,7 @@ interface AuthState {
|
||||
|
||||
const initialState: AuthState = {
|
||||
isAuthenticated: localStorage.getItem("isAuthenticated") === "true",
|
||||
userId: null,
|
||||
userId: localStorage.getItem("userId") ? parseInt(localStorage.getItem("userId")!, 10) : null,
|
||||
username: localStorage.getItem("username") || null,
|
||||
loading: false,
|
||||
};
|
||||
@ -26,6 +26,7 @@ const authSlice = createSlice({
|
||||
state.userId = action.payload.userId;
|
||||
state.username = action.payload.username;
|
||||
localStorage.setItem("isAuthenticated", "true");
|
||||
localStorage.setItem("userId", action.payload.userId.toString());
|
||||
localStorage.setItem("username", action.payload.username);
|
||||
},
|
||||
clearAuth: (state) => {
|
||||
@ -33,6 +34,7 @@ const authSlice = createSlice({
|
||||
state.userId = null;
|
||||
state.username = null;
|
||||
localStorage.removeItem("isAuthenticated");
|
||||
localStorage.removeItem("userId");
|
||||
localStorage.removeItem("username");
|
||||
},
|
||||
},
|
||||
|
||||
@ -1612,22 +1612,29 @@ textarea:focus {
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
/* Плавающая панель форматирования */
|
||||
/* Плавающая панель форматирования - улучшенная версия */
|
||||
.floating-toolbar-wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
/* Скрываем скроллбар */
|
||||
scrollbar-width: none;
|
||||
/* Плавная прокрутка */
|
||||
scroll-behavior: smooth;
|
||||
/* Максимальная ширина с учетом отступов */
|
||||
max-width: calc(100vw - 20px);
|
||||
/* Предотвращаем выбор текста при перетаскивании */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
/* Улучшаем производительность */
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.floating-toolbar-wrapper::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Мобильная версия */
|
||||
.floating-toolbar-wrapper.mobile {
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
|
||||
.floating-toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
@ -1638,7 +1645,6 @@ textarea:focus {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
align-items: center;
|
||||
/* Предотвращаем сжатие кнопок */
|
||||
flex-shrink: 0;
|
||||
min-width: fit-content;
|
||||
width: max-content;
|
||||
@ -1669,6 +1675,28 @@ textarea:focus {
|
||||
transition: all 0.2s ease;
|
||||
min-width: 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 {
|
||||
|
||||
@ -45,9 +45,22 @@ const renderer: any = {
|
||||
},
|
||||
// Кастомный renderer для элементов списка с чекбоксами
|
||||
listitem(token: any) {
|
||||
const text = token.text;
|
||||
const task = token.task;
|
||||
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) {
|
||||
const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} />`;
|
||||
|
||||
@ -39,6 +39,8 @@ export function waitForIndexedDB(): Promise<IDBDatabase> {
|
||||
|
||||
/**
|
||||
* Проверка состояния сети (более надежный метод)
|
||||
* Не блокирует выполнение, всегда возвращает результат
|
||||
* Различает "нет интернета" (offline) и "не авторизован" (401)
|
||||
*/
|
||||
export async function checkNetworkStatus(): Promise<boolean> {
|
||||
// Простая проверка navigator.onLine
|
||||
@ -49,7 +51,8 @@ export async function checkNetworkStatus(): Promise<boolean> {
|
||||
// Дополнительная проверка через fetch с коротким таймаутом
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
||||
// Уменьшаем таймаут для более быстрого ответа
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
|
||||
// Используем /auth/status так как он всегда доступен при наличии сети
|
||||
const response = await fetch('/api/auth/status', {
|
||||
@ -60,10 +63,27 @@ export async function checkNetworkStatus(): Promise<boolean> {
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
return response.ok;
|
||||
|
||||
// Если получили ответ (даже 401), значит интернет есть
|
||||
// 401 означает "не авторизован", но НЕ "нет интернета"
|
||||
// Любой ответ (даже ошибка) означает, что сеть работает
|
||||
return true;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,10 @@ export default defineConfig({
|
||||
"icon.svg",
|
||||
"icons/icon-192x192.png",
|
||||
"icons/icon-512x512.png",
|
||||
"manifest.json",
|
||||
],
|
||||
// Включаем стратегию для offline работы
|
||||
strategies: "generateSW",
|
||||
manifest: {
|
||||
name: "NoteJS - Система заметок",
|
||||
short_name: "NoteJS",
|
||||
@ -88,8 +91,41 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot}"],
|
||||
// Кэшируем все статические ресурсы для offline работы
|
||||
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot,json}"],
|
||||
// Стратегия для главной страницы - CacheFirst для offline работы
|
||||
navigateFallback: "/index.html",
|
||||
navigateFallbackDenylist: [/^\/api/, /^\/_/],
|
||||
// Кэширование для offline работы приложения
|
||||
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\./,
|
||||
handler: "NetworkFirst",
|
||||
@ -99,6 +135,7 @@ export default defineConfig({
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60, // 1 hour
|
||||
},
|
||||
networkTimeoutSeconds: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -110,7 +147,7 @@ export default defineConfig({
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 24 * 60 * 60, // 24 hours
|
||||
},
|
||||
networkTimeoutSeconds: 10,
|
||||
networkTimeoutSeconds: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user