Вернул старый оффлайн режим когда он не работал
This commit is contained in:
parent
5b3e41d1b6
commit
87a01629ae
@ -81,34 +81,15 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
|
||||
"url": "registerSW.js",
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "/index.html",
|
||||
"revision": "0.ri1maclacqo"
|
||||
"url": "index.html",
|
||||
"revision": "0.fijdulj6fg"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), {
|
||||
allowlist: [/^\/$/],
|
||||
denylist: [/^\/api/, /^\/_/]
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
allowlist: [/^\/$/]
|
||||
}));
|
||||
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({
|
||||
"cacheName": "api-cache",
|
||||
"networkTimeoutSeconds": 3,
|
||||
plugins: [new workbox.ExpirationPlugin({
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 3600
|
||||
@ -116,7 +97,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
|
||||
}), 'GET');
|
||||
workbox.registerRoute(/\/api\//, new workbox.NetworkFirst({
|
||||
"cacheName": "api-cache-local",
|
||||
"networkTimeoutSeconds": 3,
|
||||
"networkTimeoutSeconds": 10,
|
||||
plugins: [new workbox.ExpirationPlugin({
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 86400
|
||||
|
||||
@ -19,12 +19,7 @@ const AppContent: React.FC = () => {
|
||||
<>
|
||||
<NotificationStack />
|
||||
<InstallPrompt />
|
||||
<BrowserRouter
|
||||
future={{
|
||||
v7_startTransition: true,
|
||||
v7_relativeSplatPath: true,
|
||||
}}
|
||||
>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
|
||||
@ -34,31 +34,22 @@ 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)
|
||||
);
|
||||
|
||||
// Разлогиниваем только если это НЕ запрос с проверкой пароля/статуса
|
||||
// и только если есть ответ от сервера (не offline режим)
|
||||
if (!isPasswordProtected && error.response) {
|
||||
// Разлогиниваем только если это НЕ запрос с проверкой пароля
|
||||
if (!isPasswordProtected) {
|
||||
localStorage.removeItem("isAuthenticated");
|
||||
localStorage.removeItem("userId");
|
||||
localStorage.removeItem("username");
|
||||
// Не делаем редирект, если нет интернета (error.response будет undefined)
|
||||
// ProtectedRoute сам обработает это
|
||||
if (navigator.onLine) {
|
||||
window.location.href = "/";
|
||||
}
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,7 @@ export const notesApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
unarchive: async (id: number | string) => {
|
||||
unarchive: async (id: number) => {
|
||||
const { data } = await axiosClient.put(`/notes/${id}/unarchive`);
|
||||
return data;
|
||||
},
|
||||
@ -89,7 +89,7 @@ export const notesApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
deleteArchived: async (id: number | string) => {
|
||||
deleteArchived: async (id: number) => {
|
||||
await axiosClient.delete(`/notes/archived/${id}`);
|
||||
},
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@ export const userApi = {
|
||||
currentPassword?: string;
|
||||
newPassword?: string;
|
||||
accent_color?: string;
|
||||
show_edit_date?: boolean | number;
|
||||
colored_icons?: boolean | number;
|
||||
show_edit_date?: boolean;
|
||||
colored_icons?: boolean;
|
||||
}
|
||||
) => {
|
||||
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 { authApi } from "../api/authApi";
|
||||
import { LoadingOverlay } from "./common/LoadingOverlay";
|
||||
import { checkNetworkStatus } from "../utils/offlineManager";
|
||||
|
||||
export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
@ -16,84 +15,26 @@ export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
// Проверяем, есть ли интернет
|
||||
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) {
|
||||
const authStatus = await authApi.checkStatus();
|
||||
if (authStatus.authenticated) {
|
||||
dispatch(
|
||||
setAuth({
|
||||
userId: parseInt(savedUserId, 10),
|
||||
username: savedUsername,
|
||||
userId: authStatus.userId!,
|
||||
username: authStatus.username!,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(clearAuth());
|
||||
}
|
||||
} catch {
|
||||
dispatch(clearAuth());
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Всегда проверяем статус аутентификации при монтировании,
|
||||
// независимо от начального состояния Redux (localStorage может быть устаревшим)
|
||||
checkAuth();
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
format,
|
||||
startOfMonth,
|
||||
|
||||
@ -2,6 +2,7 @@ import React from "react";
|
||||
import { MiniCalendar } from "../calendar/MiniCalendar";
|
||||
import { SearchBar } from "../search/SearchBar";
|
||||
import { TagsFilter } from "../search/TagsFilter";
|
||||
import { useAppSelector } from "../../store/hooks";
|
||||
import { Note } from "../../types/note";
|
||||
|
||||
interface SidebarProps {
|
||||
|
||||
@ -113,14 +113,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||||
};
|
||||
}, [isDragging]);
|
||||
|
||||
const buttons: Array<{
|
||||
id: string;
|
||||
action?: () => void;
|
||||
before?: string;
|
||||
after?: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
}> = [];
|
||||
const buttons = [];
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -149,7 +142,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||||
}}
|
||||
title={btn.title}
|
||||
>
|
||||
{btn.icon && <Icon icon={btn.icon} />}
|
||||
<Icon icon={btn.icon} />
|
||||
</button>
|
||||
))}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { FloatingToolbar } from "./FloatingToolbar";
|
||||
import { NotePreview } from "./NotePreview";
|
||||
import { ImageUpload } from "./ImageUpload";
|
||||
import { FileUpload } from "./FileUpload";
|
||||
import { useAppSelector } from "../../store/hooks";
|
||||
import { useAppSelector, useAppDispatch } from "../../store/hooks";
|
||||
import { useNotification } from "../../hooks/useNotification";
|
||||
import { offlineNotesApi } from "../../api/offlineNotesApi";
|
||||
import { aiApi } from "../../api/aiApi";
|
||||
@ -31,6 +31,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
const isPreviewMode = useAppSelector((state) => state.ui.isPreviewMode);
|
||||
const { showNotification } = useNotification();
|
||||
const aiEnabled = useAppSelector((state) => state.profile.aiEnabled);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!content.trim()) {
|
||||
@ -250,27 +251,36 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
|
||||
// Определяем, есть ли уже такие маркеры на всех строках
|
||||
let allLinesHaveMarker = true;
|
||||
let hasAnyMarker = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trimStart();
|
||||
if (before === "- ") {
|
||||
// Для маркированного списка проверяем различные варианты
|
||||
if (!trimmedLine.match(/^[-*+]\s/)) {
|
||||
if (trimmedLine.match(/^[-*+]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "1. ") {
|
||||
// Для нумерованного списка
|
||||
if (!trimmedLine.match(/^\d+\.\s/)) {
|
||||
if (trimmedLine.match(/^\d+\.\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "- [ ] ") {
|
||||
// Для чекбокса
|
||||
if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "> ") {
|
||||
// Для цитаты
|
||||
if (!trimmedLine.startsWith("> ")) {
|
||||
if (trimmedLine.startsWith("> ")) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
}
|
||||
@ -520,12 +530,14 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
const lines = text.split("\n");
|
||||
|
||||
// Определяем текущую строку
|
||||
let currentLineIndex = 0;
|
||||
let currentLineStart = 0;
|
||||
let currentLine = "";
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineLength = lines[i].length;
|
||||
if (currentLineStart + lineLength >= start) {
|
||||
currentLineIndex = i;
|
||||
currentLine = lines[i];
|
||||
break;
|
||||
}
|
||||
@ -656,6 +668,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
const lineHeight = parseInt(styles.lineHeight) || 20;
|
||||
const paddingTop = parseInt(styles.paddingTop) || 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> = ({
|
||||
note,
|
||||
onDelete: _onDelete,
|
||||
onDelete,
|
||||
onPin,
|
||||
onArchive,
|
||||
onReload,
|
||||
@ -41,8 +41,8 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const [showArchiveModal, setShowArchiveModal] = useState(false);
|
||||
const [editImages, setEditImages] = useState<File[]>([]);
|
||||
const [editFiles, setEditFiles] = useState<File[]>([]);
|
||||
const [deletedImageIds, setDeletedImageIds] = useState<(number | string)[]>([]);
|
||||
const [deletedFileIds, setDeletedFileIds] = useState<(number | string)[]>([]);
|
||||
const [deletedImageIds, setDeletedImageIds] = useState<number[]>([]);
|
||||
const [deletedFileIds, setDeletedFileIds] = useState<number[]>([]);
|
||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||
const [toolbarPosition, setToolbarPosition] = useState({ top: 0, left: 0 });
|
||||
@ -140,19 +140,19 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
setLocalPreviewMode(false);
|
||||
};
|
||||
|
||||
const handleDeleteExistingImage = (imageId: number | string) => {
|
||||
const handleDeleteExistingImage = (imageId: number) => {
|
||||
setDeletedImageIds([...deletedImageIds, imageId]);
|
||||
};
|
||||
|
||||
const handleDeleteExistingFile = (fileId: number | string) => {
|
||||
const handleDeleteExistingFile = (fileId: number) => {
|
||||
setDeletedFileIds([...deletedFileIds, fileId]);
|
||||
};
|
||||
|
||||
const handleRestoreImage = (imageId: number | string) => {
|
||||
const handleRestoreImage = (imageId: number) => {
|
||||
setDeletedImageIds(deletedImageIds.filter((id) => id !== imageId));
|
||||
};
|
||||
|
||||
const handleRestoreFile = (fileId: number | string) => {
|
||||
const handleRestoreFile = (fileId: number) => {
|
||||
setDeletedFileIds(deletedFileIds.filter((id) => id !== fileId));
|
||||
};
|
||||
|
||||
@ -337,27 +337,36 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
|
||||
// Определяем, есть ли уже такие маркеры на всех строках
|
||||
let allLinesHaveMarker = true;
|
||||
let hasAnyMarker = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trimStart();
|
||||
if (before === "- ") {
|
||||
// Для маркированного списка проверяем различные варианты
|
||||
if (!trimmedLine.match(/^[-*+]\s/)) {
|
||||
if (trimmedLine.match(/^[-*+]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "1. ") {
|
||||
// Для нумерованного списка
|
||||
if (!trimmedLine.match(/^\d+\.\s/)) {
|
||||
if (trimmedLine.match(/^\d+\.\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "- [ ] ") {
|
||||
// Для чекбокса
|
||||
if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "> ") {
|
||||
// Для цитаты
|
||||
if (!trimmedLine.startsWith("> ")) {
|
||||
if (trimmedLine.startsWith("> ")) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
}
|
||||
@ -619,6 +628,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const lineHeight = parseInt(styles.lineHeight) || 20;
|
||||
const paddingTop = parseInt(styles.paddingTop) || 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");
|
||||
|
||||
// Определяем текущую строку
|
||||
let currentLineIndex = 0;
|
||||
let currentLineStart = 0;
|
||||
let currentLine = "";
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const lineLength = lines[i].length;
|
||||
if (currentLineStart + lineLength >= start) {
|
||||
currentLineIndex = i;
|
||||
currentLine = lines[i];
|
||||
break;
|
||||
}
|
||||
@ -1374,6 +1386,11 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
{note.files
|
||||
.filter((file) => !deletedFileIds.includes(file.id))
|
||||
.map((file) => {
|
||||
const fileUrl = getFileUrl(
|
||||
file.file_path,
|
||||
note.id,
|
||||
file.id
|
||||
);
|
||||
return (
|
||||
<div key={file.id} className="file-preview-item">
|
||||
<Icon
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useImperativeHandle, forwardRef } from "react";
|
||||
import React, { useEffect, useImperativeHandle, forwardRef } from "react";
|
||||
import { NoteItem } from "./NoteItem";
|
||||
import { useAppSelector, useAppDispatch } from "../../store/hooks";
|
||||
import { offlineNotesApi } from "../../api/offlineNotesApi";
|
||||
@ -10,7 +10,7 @@ export interface NotesListRef {
|
||||
reloadNotes: () => void;
|
||||
}
|
||||
|
||||
export const NotesList = forwardRef<NotesListRef>((_props, ref) => {
|
||||
export const NotesList = forwardRef<NotesListRef>((props, ref) => {
|
||||
const notes = useAppSelector((state) => state.notes.notes);
|
||||
const userId = useAppSelector((state) => state.auth.userId);
|
||||
const searchQuery = useAppSelector((state) => state.notes.searchQuery);
|
||||
|
||||
@ -6,7 +6,7 @@ import { setSearchQuery } from "../../store/slices/notesSlice";
|
||||
export const SearchBar: React.FC = () => {
|
||||
const [query, setQuery] = useState("");
|
||||
const dispatch = useAppDispatch();
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Debounce для поиска
|
||||
|
||||
@ -10,8 +10,7 @@ export const useNotification = () => {
|
||||
message: string,
|
||||
type: "info" | "success" | "error" | "warning" = "info"
|
||||
) => {
|
||||
const id = `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
dispatch(addNotification({ id, message, type }));
|
||||
const id = dispatch(addNotification({ message, type })).payload.id;
|
||||
setTimeout(() => {
|
||||
dispatch(removeNotification(id));
|
||||
}, 4000);
|
||||
|
||||
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,46 +23,28 @@ async function initOfflineMode() {
|
||||
await dbManager.init();
|
||||
console.log('[Init] IndexedDB initialized');
|
||||
|
||||
// Устанавливаем начальное состояние на основе 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));
|
||||
});
|
||||
// Проверка состояния сети
|
||||
const isOnline = await checkNetworkStatus();
|
||||
store.dispatch(setOfflineMode(!isOnline));
|
||||
console.log(`[Init] Network status: ${isOnline ? 'online' : 'offline'}`);
|
||||
|
||||
// Установка listeners для событий сети
|
||||
networkListener.onOnline(async () => {
|
||||
console.log('[Network] Online event detected');
|
||||
// Небольшая задержка перед проверкой для стабилизации соединения
|
||||
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);
|
||||
const isOnline = await checkNetworkStatus();
|
||||
store.dispatch(setOfflineMode(!isOnline));
|
||||
|
||||
if (isOnline) {
|
||||
store.dispatch(
|
||||
addNotification({
|
||||
message: 'Подключение восстановлено, начинаем синхронизацию...',
|
||||
type: 'info',
|
||||
})
|
||||
);
|
||||
|
||||
// Запуск синхронизации
|
||||
await syncService.startSync();
|
||||
}
|
||||
});
|
||||
|
||||
networkListener.onOffline(() => {
|
||||
@ -77,40 +59,29 @@ async function initOfflineMode() {
|
||||
});
|
||||
|
||||
// Обновление счетчика ожидающих синхронизацию
|
||||
dbManager.getPendingSyncCount().then((pendingCount) => {
|
||||
store.dispatch(setPendingSyncCount(pendingCount));
|
||||
|
||||
if (pendingCount > 0) {
|
||||
console.log(`[Init] Found ${pendingCount} pending sync items`);
|
||||
}
|
||||
const pendingCount = await dbManager.getPendingSyncCount();
|
||||
store.dispatch(setPendingSyncCount(pendingCount));
|
||||
|
||||
if (pendingCount > 0) {
|
||||
console.log(`[Init] Found ${pendingCount} pending sync items`);
|
||||
}
|
||||
|
||||
// Автоматическая синхронизация при старте если есть что синхронизировать
|
||||
// Проверяем статус сети перед синхронизацией
|
||||
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);
|
||||
});
|
||||
// Автоматическая синхронизация при старте если есть что синхронизировать
|
||||
if (isOnline && pendingCount > 0) {
|
||||
console.log('[Init] Starting initial sync...');
|
||||
// Небольшая задержка для инициализации UI
|
||||
setTimeout(() => {
|
||||
syncService.startSync();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useAppDispatch } from "../store/hooks";
|
||||
import { useAppSelector, useAppDispatch } from "../store/hooks";
|
||||
import { userApi } from "../api/userApi";
|
||||
import { authApi } from "../api/authApi";
|
||||
import { clearAuth } from "../store/slices/authSlice";
|
||||
@ -16,6 +16,7 @@ const ProfilePage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const { showNotification } = useNotification();
|
||||
const user = useAppSelector((state) => state.profile.user);
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
@ -13,6 +13,7 @@ import { setAccentColor } from "../utils/colorUtils";
|
||||
import { useNotification } from "../hooks/useNotification";
|
||||
import { Modal } from "../components/common/Modal";
|
||||
import { ThemeToggle } from "../components/common/ThemeToggle";
|
||||
import { formatDateFromTimestamp } from "../utils/dateFormat";
|
||||
import { parseMarkdown } from "../utils/markdown";
|
||||
import { dbManager } from "../utils/indexedDB";
|
||||
import { syncService } from "../services/syncService";
|
||||
@ -25,6 +26,7 @@ const SettingsPage: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { showNotification } = useNotification();
|
||||
const user = useAppSelector((state) => state.profile.user);
|
||||
const accentColor = useAppSelector((state) => state.ui.accentColor);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>(() => {
|
||||
// Восстанавливаем активную вкладку из localStorage при инициализации
|
||||
@ -162,8 +164,8 @@ const SettingsPage: React.FC = () => {
|
||||
try {
|
||||
await userApi.updateProfile({
|
||||
accent_color: selectedAccentColor,
|
||||
show_edit_date: showEditDate ? 1 : 0,
|
||||
colored_icons: coloredIcons ? 1 : 0,
|
||||
show_edit_date: showEditDate,
|
||||
colored_icons: coloredIcons,
|
||||
});
|
||||
dispatch(setAccentColorAction(selectedAccentColor));
|
||||
setAccentColor(selectedAccentColor);
|
||||
@ -271,7 +273,7 @@ const SettingsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreNote = async (id: number | string) => {
|
||||
const handleRestoreNote = async (id: number) => {
|
||||
try {
|
||||
await notesApi.unarchive(id);
|
||||
await loadArchivedNotes();
|
||||
@ -285,7 +287,7 @@ const SettingsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePermanent = async (id: number | string) => {
|
||||
const handleDeletePermanent = async (id: number) => {
|
||||
try {
|
||||
await notesApi.deleteArchived(id);
|
||||
await loadArchivedNotes();
|
||||
@ -418,8 +420,8 @@ const SettingsPage: React.FC = () => {
|
||||
|
||||
// Загружаем версию из IndexedDB
|
||||
try {
|
||||
const userId = (user as any)?.id;
|
||||
const localVer = userId && typeof userId === 'number'
|
||||
const userId = user?.id;
|
||||
const localVer = userId
|
||||
? await dbManager.getDataVersionByUserId(userId)
|
||||
: await dbManager.getDataVersion();
|
||||
setIndexedDBVersion(localVer);
|
||||
|
||||
@ -5,11 +5,13 @@ import { Note, NoteImage, NoteFile } from '../types/note';
|
||||
import { store } from '../store/index';
|
||||
import {
|
||||
setSyncStatus,
|
||||
removeNotification,
|
||||
addNotification,
|
||||
} from '../store/slices/uiSlice';
|
||||
import {
|
||||
updateNote,
|
||||
setPendingSyncCount,
|
||||
setOfflineMode,
|
||||
} from '../store/slices/notesSlice';
|
||||
import { SyncQueueItem } from '../types/note';
|
||||
|
||||
@ -18,7 +20,7 @@ const RETRY_DELAY_MS = 5000;
|
||||
|
||||
class SyncService {
|
||||
private isSyncing = false;
|
||||
private syncTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private syncTimer: NodeJS.Timeout | null = null;
|
||||
private listeners: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
@ -442,7 +444,7 @@ class SyncService {
|
||||
*/
|
||||
private async updateImageReferences(
|
||||
localNote: Note,
|
||||
_serverNote: Note
|
||||
serverNote: Note
|
||||
): Promise<NoteImage[]> {
|
||||
// Если нет изображений с base64, возвращаем как есть
|
||||
const hasBase64Images = localNote.images.some((img) => img.base64Data);
|
||||
@ -459,7 +461,7 @@ class SyncService {
|
||||
*/
|
||||
private async updateFileReferences(
|
||||
localNote: Note,
|
||||
_serverNote: Note
|
||||
serverNote: Note
|
||||
): Promise<NoteFile[]> {
|
||||
// Если нет файлов с base64, возвращаем как есть
|
||||
const hasBase64Files = localNote.files.some((file) => file.base64Data);
|
||||
|
||||
@ -9,7 +9,7 @@ interface AuthState {
|
||||
|
||||
const initialState: AuthState = {
|
||||
isAuthenticated: localStorage.getItem("isAuthenticated") === "true",
|
||||
userId: localStorage.getItem("userId") ? parseInt(localStorage.getItem("userId")!, 10) : null,
|
||||
userId: null,
|
||||
username: localStorage.getItem("username") || null,
|
||||
loading: false,
|
||||
};
|
||||
@ -26,7 +26,6 @@ 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) => {
|
||||
@ -34,7 +33,6 @@ const authSlice = createSlice({
|
||||
state.userId = null;
|
||||
state.username = null;
|
||||
localStorage.removeItem("isAuthenticated");
|
||||
localStorage.removeItem("userId");
|
||||
localStorage.removeItem("username");
|
||||
},
|
||||
},
|
||||
|
||||
@ -50,11 +50,9 @@ const uiSlice = createSlice({
|
||||
},
|
||||
addNotification: (
|
||||
state,
|
||||
action: PayloadAction<Omit<Notification, "id"> | Notification>
|
||||
action: PayloadAction<Omit<Notification, "id">>
|
||||
) => {
|
||||
const id = ('id' in action.payload && action.payload.id)
|
||||
? action.payload.id
|
||||
: `notification-${Date.now()}-${Math.random()
|
||||
const id = `notification-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9)}`;
|
||||
state.notifications.push({ ...action.payload, id });
|
||||
|
||||
@ -118,6 +118,7 @@ body {
|
||||
button {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-moz-tap-highlight-color: transparent;
|
||||
tap-highlight-color: transparent;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
@ -136,6 +137,7 @@ a,
|
||||
div[role="button"] {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-moz-tap-highlight-color: transparent;
|
||||
tap-highlight-color: transparent;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
*/
|
||||
export function getImageUrl(
|
||||
filePath: string,
|
||||
noteId: number | string,
|
||||
imageId: number | string
|
||||
noteId: number,
|
||||
imageId: number
|
||||
): string {
|
||||
// Если путь уже является полным URL (начинается с http:// или https://)
|
||||
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
||||
@ -47,8 +47,8 @@ export function getImageUrl(
|
||||
*/
|
||||
export function getFileUrl(
|
||||
filePath: string,
|
||||
noteId: number | string,
|
||||
fileId: number | string
|
||||
noteId: number,
|
||||
fileId: number
|
||||
): string {
|
||||
// Если путь уже является полным URL (начинается с http:// или https://)
|
||||
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
||||
|
||||
@ -24,113 +24,50 @@ const spoilerExtension = {
|
||||
};
|
||||
|
||||
// Кастомный 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 {
|
||||
// Разделяем токены на inline и блоковые
|
||||
const inlineTokens: any[] = [];
|
||||
const blockTokens: any[] = [];
|
||||
|
||||
tokens.forEach((t: any) => {
|
||||
if (blockTypes.includes(t.type)) {
|
||||
blockTokens.push(t);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Обрабатываем блоковые токены через parser
|
||||
let blockText = '';
|
||||
if (blockTokens.length > 0) {
|
||||
try {
|
||||
blockText = this.parser.parse(blockTokens);
|
||||
} catch (blockError) {
|
||||
// Если ошибка при обработке блоков, обрабатываем через стандартный 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;
|
||||
|
||||
// Если после обработки текст пустой, используем fallback
|
||||
if (!text || text.trim() === '') {
|
||||
text = token.text || '';
|
||||
}
|
||||
} catch (error) {
|
||||
// Если общая ошибка, используем fallback - обрабатываем через стандартный parser
|
||||
try {
|
||||
text = this.parser.parse(tokens);
|
||||
} catch (parseError) {
|
||||
// Последний fallback - используем raw text
|
||||
console.warn('Error parsing list item tokens:', parseError);
|
||||
text = token.text || token.raw || '';
|
||||
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 `<a href="${href}"${title ? ` title="${title}"` : ""}>${text}</a>`;
|
||||
},
|
||||
// Кастомный renderer для элементов списка с чекбоксами
|
||||
listitem(token: any) {
|
||||
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 || '';
|
||||
}
|
||||
} else {
|
||||
text = token.text || '';
|
||||
}
|
||||
|
||||
// Если это задача (чекбокс), добавляем чекбокс
|
||||
if (task) {
|
||||
const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} disabled />`;
|
||||
return `<li class="task-list-item">${checkbox} ${text}</li>\n`;
|
||||
}
|
||||
|
||||
return `<li>${text}</li>\n`;
|
||||
|
||||
if (task) {
|
||||
const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} />`;
|
||||
return `<li class="task-list-item">${checkbox} ${text}</li>\n`;
|
||||
}
|
||||
return `<li>${text}</li>\n`;
|
||||
},
|
||||
};
|
||||
|
||||
// Настройка marked
|
||||
|
||||
@ -39,8 +39,6 @@ export function waitForIndexedDB(): Promise<IDBDatabase> {
|
||||
|
||||
/**
|
||||
* Проверка состояния сети (более надежный метод)
|
||||
* Не блокирует выполнение, всегда возвращает результат
|
||||
* Различает "нет интернета" (offline) и "не авторизован" (401)
|
||||
*/
|
||||
export async function checkNetworkStatus(): Promise<boolean> {
|
||||
// Простая проверка navigator.onLine
|
||||
@ -51,11 +49,10 @@ export async function checkNetworkStatus(): Promise<boolean> {
|
||||
// Дополнительная проверка через fetch с коротким таймаутом
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
// Уменьшаем таймаут для более быстрого ответа
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
||||
|
||||
// Используем /auth/status так как он всегда доступен при наличии сети
|
||||
await fetch('/api/auth/status', {
|
||||
const response = await fetch('/api/auth/status', {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
cache: 'no-cache',
|
||||
@ -63,27 +60,10 @@ export async function checkNetworkStatus(): Promise<boolean> {
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Если получили ответ (даже 401), значит интернет есть
|
||||
// 401 означает "не авторизован", но НЕ "нет интернета"
|
||||
// Любой ответ (даже ошибка) означает, что сеть работает
|
||||
return true;
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
// Если запрос не удался из-за таймаута или сетевой ошибки
|
||||
// (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;
|
||||
}
|
||||
}
|
||||
// В остальных случаях считаем, что интернет есть
|
||||
// (возможно, просто ошибка авторизации или другая проблема)
|
||||
// Если запрос не удался, но navigator.onLine = true, считаем что онлайн
|
||||
// (возможно, просто таймаут или другая проблема)
|
||||
return navigator.onLine;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,7 @@ export default defineConfig({
|
||||
"icon.svg",
|
||||
"icons/icon-192x192.png",
|
||||
"icons/icon-512x512.png",
|
||||
"manifest.json",
|
||||
],
|
||||
// Включаем стратегию для offline работы
|
||||
strategies: "generateSW",
|
||||
manifest: {
|
||||
name: "NoteJS - Система заметок",
|
||||
short_name: "NoteJS",
|
||||
@ -91,41 +88,8 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
// Кэшируем все статические ресурсы для offline работы
|
||||
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot,json}"],
|
||||
// Стратегия для главной страницы - CacheFirst для offline работы
|
||||
navigateFallback: "/index.html",
|
||||
navigateFallbackDenylist: [/^\/api/, /^\/_/],
|
||||
// Кэширование для offline работы приложения
|
||||
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot}"],
|
||||
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",
|
||||
@ -135,7 +99,6 @@ export default defineConfig({
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60, // 1 hour
|
||||
},
|
||||
networkTimeoutSeconds: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -147,7 +110,7 @@ export default defineConfig({
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 24 * 60 * 60, // 24 hours
|
||||
},
|
||||
networkTimeoutSeconds: 3,
|
||||
networkTimeoutSeconds: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user