= ({
}}
title={btn.title}
>
- {btn.icon &&
}
+
))}
diff --git a/src/components/notes/NoteEditor.tsx b/src/components/notes/NoteEditor.tsx
index 78fec79..f59d1f7 100644
--- a/src/components/notes/NoteEditor.tsx
+++ b/src/components/notes/NoteEditor.tsx
@@ -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
= ({ 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 = ({ 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 = ({ 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 = ({ 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;
// Более точный расчет ширины символа
// Создаем временный элемент для измерения
diff --git a/src/components/notes/NoteItem.tsx b/src/components/notes/NoteItem.tsx
index 9eeb4f2..233691e 100644
--- a/src/components/notes/NoteItem.tsx
+++ b/src/components/notes/NoteItem.tsx
@@ -31,7 +31,7 @@ interface NoteItemProps {
export const NoteItem: React.FC = ({
note,
- onDelete: _onDelete,
+ onDelete,
onPin,
onArchive,
onReload,
@@ -41,8 +41,8 @@ export const NoteItem: React.FC = ({
const [showArchiveModal, setShowArchiveModal] = useState(false);
const [editImages, setEditImages] = useState([]);
const [editFiles, setEditFiles] = useState([]);
- const [deletedImageIds, setDeletedImageIds] = useState<(number | string)[]>([]);
- const [deletedFileIds, setDeletedFileIds] = useState<(number | string)[]>([]);
+ const [deletedImageIds, setDeletedImageIds] = useState([]);
+ const [deletedFileIds, setDeletedFileIds] = useState([]);
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 = ({
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 = ({
// Определяем, есть ли уже такие маркеры на всех строках
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 = ({
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 = ({
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 = ({
{note.files
.filter((file) => !deletedFileIds.includes(file.id))
.map((file) => {
+ const fileUrl = getFileUrl(
+ file.file_path,
+ note.id,
+ file.id
+ );
return (
void;
}
-export const NotesList = forwardRef((_props, ref) => {
+export const NotesList = forwardRef((props, ref) => {
const notes = useAppSelector((state) => state.notes.notes);
const userId = useAppSelector((state) => state.auth.userId);
const searchQuery = useAppSelector((state) => state.notes.searchQuery);
diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx
index f5d6960..25d067e 100644
--- a/src/components/search/SearchBar.tsx
+++ b/src/components/search/SearchBar.tsx
@@ -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 | null>(null);
+ const timeoutRef = useRef(null);
useEffect(() => {
// Debounce для поиска
diff --git a/src/hooks/useNotification.ts b/src/hooks/useNotification.ts
index 2e455bc..f7e8422 100644
--- a/src/hooks/useNotification.ts
+++ b/src/hooks/useNotification.ts
@@ -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);
diff --git a/src/main.tsx b/src/main.tsx
index ea594df..bce2974 100644
--- a/src/main.tsx
+++ b/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(
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index 970a44a..8a87862 100644
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -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("");
diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx
index 5d35bcb..157b8f2 100644
--- a/src/pages/SettingsPage.tsx
+++ b/src/pages/SettingsPage.tsx
@@ -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(() => {
// Восстанавливаем активную вкладку из 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);
diff --git a/src/services/syncService.ts b/src/services/syncService.ts
index c08f2f7..45e023c 100644
--- a/src/services/syncService.ts
+++ b/src/services/syncService.ts
@@ -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 | 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 {
// Если нет изображений с 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 {
// Если нет файлов с base64, возвращаем как есть
const hasBase64Files = localNote.files.some((file) => file.base64Data);
diff --git a/src/store/slices/authSlice.ts b/src/store/slices/authSlice.ts
index 8742eec..138a0cb 100644
--- a/src/store/slices/authSlice.ts
+++ b/src/store/slices/authSlice.ts
@@ -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");
},
},
diff --git a/src/store/slices/uiSlice.ts b/src/store/slices/uiSlice.ts
index d7b1d40..980460b 100644
--- a/src/store/slices/uiSlice.ts
+++ b/src/store/slices/uiSlice.ts
@@ -50,11 +50,9 @@ const uiSlice = createSlice({
},
addNotification: (
state,
- action: PayloadAction | Notification>
+ action: PayloadAction>
) => {
- 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 });
diff --git a/src/styles/style.css b/src/styles/style.css
index e071cf4..32f9652 100644
--- a/src/styles/style.css
+++ b/src/styles/style.css
@@ -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;
}
diff --git a/src/utils/filePaths.ts b/src/utils/filePaths.ts
index 9962a4a..40b34c7 100644
--- a/src/utils/filePaths.ts
+++ b/src/utils/filePaths.ts
@@ -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://")) {
diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts
index 409690f..6f5f4df 100644
--- a/src/utils/markdown.ts
+++ b/src/utils/markdown.ts
@@ -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 `${text}`;
- }
- } 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 `${text}`;
}
+ } catch {}
+
+ return `${text}`;
+ },
+ // Кастомный 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 = ``;
- return `${checkbox} ${text}\n`;
- }
-
- return `${text}\n`;
+
+ if (task) {
+ const checkbox = ``;
+ return `${checkbox} ${text}\n`;
+ }
+ return `${text}\n`;
+ },
};
// Настройка marked
diff --git a/src/utils/offlineManager.ts b/src/utils/offlineManager.ts
index df5025c..5bfb948 100644
--- a/src/utils/offlineManager.ts
+++ b/src/utils/offlineManager.ts
@@ -39,8 +39,6 @@ export function waitForIndexedDB(): Promise {
/**
* Проверка состояния сети (более надежный метод)
- * Не блокирует выполнение, всегда возвращает результат
- * Различает "нет интернета" (offline) и "не авторизован" (401)
*/
export async function checkNetworkStatus(): Promise {
// Простая проверка navigator.onLine
@@ -51,11 +49,10 @@ export async function checkNetworkStatus(): Promise {
// Дополнительная проверка через 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 {
});
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;
}
}
diff --git a/vite.config.ts b/vite.config.ts
index 5e1039e..d7e1ff5 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -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,
},
},
{