Compare commits

...

2 Commits

Author SHA1 Message Date
b831dcc52c Улучшена поддержка цветового акцента пользователя на страницах заметок и профиля
- Реализовано динамическое изменение цвета акцента на основе данных пользователя для предотвращения FOUC.
- Добавлен inline CSS для корректного отображения цвета акцента в HTML.
- Обновлены стили для тегов, чтобы использовать белый фон вместо светло-голубого.
2025-10-21 00:00:35 +07:00
8354e64ae7 Удалены устаревшие файлы и улучшена структура проекта
- Удалены файлы тестирования загрузки изображений и PWA, чтобы оптимизировать проект.
- Обновлены мета-теги и улучшены стили для поддержки мобильных устройств.
- Реализована функция ленивой загрузки изображений для повышения производительности.
- Добавлены новые функции для управления цветом акцента в профиле пользователя.
2025-10-20 23:14:38 +07:00
18 changed files with 831 additions and 3597 deletions

View File

@ -1,92 +0,0 @@
# 📱 Тестирование загрузки изображений на мобильных устройствах
## Проблема
Пользователи не могли загружать картинки в заметки с мобильных телефонов.
## Внесенные исправления
### 1. Улучшения JavaScript (app.js)
- ✅ Добавлена поддержка touch событий для кнопки загрузки изображений
- ✅ Улучшена обработка выбора файлов с проверкой размера и типа
- ✅ Добавлена защита от дублирования файлов
- ✅ Улучшена функция обновления превью с обработкой ошибок
- ✅ Добавлены индикаторы загрузки для мобильных устройств
- ✅ Улучшена функция сохранения заметок с уведомлениями
### 2. Улучшения CSS (style.css)
- ✅ Добавлены стили для touch устройств (touch-action, -webkit-tap-highlight-color)
- ✅ Увеличена минимальная высота кнопок для удобства touch (44px+)
- ✅ Улучшены размеры кнопок удаления изображений
- ✅ Добавлены специальные стили для мобильных устройств в медиа-запросах
### 3. Создана тестовая страница
- ✅ `/test-mobile-upload.html` - специальная страница для тестирования загрузки на мобильных
## Как протестировать
### На мобильном устройстве:
1. Откройте приложение в мобильном браузере
2. Перейдите на страницу заметок
3. Нажмите на кнопку загрузки изображений (📷)
4. Выберите одно или несколько изображений
5. Проверьте превью изображений
6. Нажмите "Сохранить"
7. Убедитесь, что изображения загрузились и отображаются в заметке
### Альтернативный способ тестирования:
1. Откройте `/test-mobile-upload.html` на мобильном устройстве
2. Эта страница содержит специальные тесты для проверки загрузки файлов
3. Проверьте все функции загрузки и отображения
## Основные улучшения для мобильных устройств
### Touch Events
- Добавлена поддержка `touchend` событий
- Улучшена обработка touch для кнопок
### Размеры элементов
- Минимальная высота кнопок: 44px (рекомендация Apple/Google)
- Увеличены размеры кнопок удаления изображений
- Улучшены отступы и размеры для touch
### Визуальная обратная связь
- Индикаторы загрузки для мобильных устройств
- Уведомления об успешном сохранении
- Обработка ошибок с понятными сообщениями
### Производительность
- Проверка размера файлов (максимум 10MB)
- Защита от дублирования файлов
- Обработка ошибок чтения файлов
## Поддерживаемые форматы изображений
- JPEG (.jpg, .jpeg)
- PNG (.png)
- GIF (.gif)
- WebP (.webp)
## Ограничения
- Максимальный размер файла: 10MB
- Максимальное количество файлов за раз: 10
- Поддерживаются только изображения
## Браузеры
Протестировано на:
- ✅ Chrome Mobile (Android)
- ✅ Safari Mobile (iOS)
- ✅ Firefox Mobile
- ✅ Samsung Internet
## Если проблемы остаются
1. **Проверьте консоль браузера** на наличие ошибок JavaScript
2. **Убедитесь, что у вас стабильное интернет-соединение**
3. **Попробуйте уменьшить размер изображений** (сжать перед загрузкой)
4. **Проверьте, что браузер поддерживает File API** (современные браузеры поддерживают)
5. **Попробуйте перезагрузить страницу** и повторить попытку
## Отладка
Для отладки используйте:
- `/test-mobile-upload.html` - специальная тестовая страница
- Консоль разработчика в мобильном браузере
- Информация об устройстве и браузере на тестовой странице

View File

@ -1,269 +0,0 @@
# 🚀 Тестирование PWA для NoteJS
## Что было сделано
✅ **Созданы файлы PWA:**
- `manifest.json` - манифест приложения
- `sw.js` - сервис-воркер для кэширования
- `pwa.js` - JavaScript класс для управления PWA
- `icon.svg` - SVG иконка приложения
- `logo.svg` - логотип приложения
- `icons/` - PNG иконки различных размеров
- `browserconfig.xml` - конфигурация для Windows
✅ **Обновлены HTML страницы:**
- Добавлены PWA мета-теги
- Подключены иконки и манифест
- Добавлен скрипт регистрации Service Worker
✅ **Настроен сервер:**
- Правильные заголовки для PWA файлов
- Поддержка кэширования
## Как протестировать
### 1. Откройте диагностическую страницу PWA
```
http://localhost:3000/pwa-debug.html
```
### 2. Откройте тестовую страницу для мобильных устройств
```
http://localhost:3000/mobile-pwa-test.html
```
### 3. Откройте тестовую страницу для Brave браузера
```
http://localhost:3000/brave-pwa-test.html
```
### 4. Откройте обычную тестовую страницу
```
http://localhost:3000/test-pwa.html
```
### 5. Проверьте требования PWA
На мобильной тестовой странице автоматически проверяются все требования:
- ✅ HTTPS или localhost
- ✅ Service Worker
- ✅ Manifest
- ✅ Иконки
- ❌ Уже установлено (должно быть красным, если не установлено)
### 6. Установка приложения
- Если все проверки пройдены, появится кнопка "Установить приложение"
- Нажмите на неё для установки PWA
- Следуйте инструкциям браузера или используйте инструкции на странице
### 7. Проверка в разных браузерах
#### Chrome/Edge:
- Откройте DevTools (F12)
- Перейдите в Application → Manifest
- Проверьте, что манифест загружается без ошибок
- В Application → Service Workers проверьте статус SW
#### Firefox:
- Откройте DevTools (F12)
- Перейдите в Application → Manifest
- Проверьте манифест
#### Brave:
- Откройте `http://localhost:3000/brave-pwa-test.html` для диагностики
- Проверьте настройки PWA в браузере
- Используйте меню браузера для установки
#### Safari (iOS):
- Откройте сайт в Safari
- Нажмите кнопку "Поделиться"
- Выберите "На экран Домой"
- Приложение установится как PWA
#### ПК/Десктоп (Chrome, Edge, Firefox):
- Откройте сайт в браузере
- Нажмите на иконку установки в адресной строке (если доступна)
- Или используйте меню браузера → "Установить приложение"
- Или используйте меню браузера → "Создать ярлык"
## Новые улучшения для мобильных устройств
✅ **Улучшенный manifest.json:**
- Добавлены все необходимые размеры иконок (72x72, 96x96, 128x128, 144x144, 152x152, 192x192, 384x384, 512x512)
- Добавлены maskable иконки для Android
- Добавлены категории и скриншоты
- Улучшена совместимость с мобильными устройствами
✅ **Улучшенные мета-теги:**
- Добавлены все необходимые apple-touch-icon размеры
- Улучшена поддержка iOS Safari
- Добавлены мета-теги для Windows
- Настроен правильный статус-бар для iOS
✅ **Улучшенный Service Worker:**
- Кэширование всех иконок
- Улучшенная обработка ошибок
- Fallback для различных типов ресурсов
- Лучшая поддержка мобильных устройств
✅ **Улучшенный PWA Manager:**
- Определение мобильного Safari
- Разные инструкции для разных браузеров
- Улучшенная проверка установки PWA
- Поддержка различных режимов отображения
- **Кнопка установки показывается только на мобильных устройствах**
- Принудительная проверка возможности установки
- Специальные инструкции для Android и iOS
- Диагностическая страница для отладки PWA
## Возможные проблемы и решения
### 1. Кнопка установки не появляется
**Причины:**
- Приложение уже установлено
- Браузер не поддерживает PWA
- Не выполнены требования PWA
- **Вы используете ПК/десктоп (кнопка скрыта для ПК)**
- **Проблемы с Brave браузером**
**Решение:**
- Проверьте статус на мобильной тестовой странице
- Убедитесь, что используете HTTPS или localhost
- Проверьте консоль браузера на ошибки
- Для iOS Safari используйте инструкции "Добавить на главный экран"
- **На ПК используйте меню браузера для установки PWA**
- **Для Brave: проверьте настройки PWA в браузере**
### 1.1. Проблемы с Brave браузером
**Причины:**
- Brave может блокировать PWA по умолчанию
- Неправильная конфигурация manifest.json
- Проблемы с Service Worker в Brave
**Решение:**
- Откройте `http://localhost:3000/brave-pwa-test.html` для диагностики
- В Brave перейдите в **Настройки → Дополнительно → Сайты и разрешения → PWA**
- Убедитесь, что PWA включены
- Попробуйте установить через меню браузера (три точки → "Установить приложение")
- Обновите manifest.json с новыми полями для Brave совместимости
### 2. Service Worker не регистрируется
**Причины:**
- Ошибки в коде SW
- Проблемы с кэшированием файлов
**Решение:**
- Откройте DevTools → Application → Service Workers
- Проверьте ошибки в консоли
- Попробуйте очистить кэш
### 3. Ошибка "Download error or resource isn't a valid image"
**Причины:**
- PNG иконки повреждены или имеют неправильный размер
- Иконки не являются валидными PNG файлами
**Решение:**
- ✅ **ИСПРАВЛЕНО**: Созданы правильные PNG иконки с помощью pngjs
- Проверьте, что иконки имеют правильные размеры (192x192, 512x512)
- Убедитесь, что файлы иконок валидные PNG
### 4. Предупреждение о deprecated meta tag
**Причины:**
- Использование устаревшего `apple-mobile-web-app-capable`
**Решение:**
- ✅ **ИСПРАВЛЕНО**: Добавлен современный `mobile-web-app-capable`
- Оба тега теперь присутствуют для совместимости
## Отладка
### Консоль браузера
Откройте DevTools (F12) и проверьте консоль на ошибки:
```javascript
// Проверить статус PWA
window.debugPWA();
// Проверить Service Worker
navigator.serviceWorker.getRegistrations().then(console.log);
// Проверить возможность установки
window.checkInstallability();
// Принудительная попытка установки
window.forceInstall();
```
### Диагностическая страница
Используйте `http://localhost:3000/pwa-debug.html` для:
- Детальной диагностики всех требований PWA
- Проверки загрузки манифеста и Service Worker
- Отображения информации о браузере и устройстве
- Просмотра ошибок консоли
- Тестирования установки PWA
### Lighthouse
Запустите аудит Lighthouse в Chrome DevTools:
1. Откройте DevTools (F12)
2. Перейдите в Lighthouse
3. Выберите "Progressive Web App"
4. Нажмите "Generate report"
## Файлы для проверки
- ✅ `http://localhost:3000/manifest.json` - должен возвращать JSON
- ✅ `http://localhost:3000/sw.js` - должен возвращать JavaScript
- ✅ `http://localhost:3000/icons/icon-192x192.png` - должен возвращать PNG
- ✅ `http://localhost:3000/icons/icon-512x512.png` - должен возвращать PNG
- ✅ `http://localhost:3000/pwa-debug.html` - диагностическая страница PWA
- ✅ `http://localhost:3000/mobile-pwa-test.html` - мобильная тестовая страница
- ✅ `http://localhost:3000/brave-pwa-test.html` - тестовая страница для Brave браузера
- ✅ `http://localhost:3000/test-pwa.html` - обычная тестовая страница
## Следующие шаги
После успешного тестирования:
1. Удалите тестовую страницу `test-pwa.html`
2. Настройте HTTPS для продакшена
3. Добавьте реальные скриншоты в манифест
4. Создайте качественные иконки с помощью дизайнера
5. Настройте push-уведомления (опционально)

View File

@ -36,6 +36,47 @@ let searchResults = [];
let notesCache = null; // Кэш для заметок
let lastLoadTime = 0; // Время последней загрузки
// Lazy loading для изображений
function initLazyLoading() {
// Проверяем поддержку Intersection Observer API
if ("IntersectionObserver" in window) {
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
// Если у изображения есть data-src, загружаем его
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute("data-src");
}
img.classList.remove("lazy");
observer.unobserve(img);
}
});
},
{
rootMargin: "50px 0px", // Загружать изображения за 50px до появления в viewport
threshold: 0.01,
}
);
// Наблюдаем за всеми изображениями с классом lazy
document.querySelectorAll("img.lazy").forEach((img) => {
imageObserver.observe(img);
});
} else {
// Fallback для старых браузеров - просто показываем все изображения
document.querySelectorAll("img.lazy").forEach((img) => {
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute("data-src");
}
img.classList.remove("lazy");
});
}
}
// Функция для получения текущей даты и времени
function getFormattedDateTime() {
let now = new Date();
@ -151,7 +192,10 @@ function renderTags() {
// Добавляем обработчики кликов для тегов
tagsContainer.querySelectorAll(".tag").forEach((tagElement) => {
tagElement.addEventListener("click", async (event) => await handleTagClick(event));
tagElement.addEventListener(
"click",
async (event) => await handleTagClick(event)
);
});
}
@ -343,20 +387,21 @@ imageBtn.addEventListener("touchend", function (event) {
imageInput.addEventListener("change", function (event) {
const files = Array.from(event.target.files);
let addedCount = 0;
files.forEach(file => {
if (file.type.startsWith('image/')) {
files.forEach((file) => {
if (file.type.startsWith("image/")) {
// Проверяем размер файла (максимум 10MB)
if (file.size > 10 * 1024 * 1024) {
alert(`Файл "${file.name}" слишком большой. Максимальный размер: 10MB`);
return;
}
// Проверяем, не добавлен ли уже этот файл
const isDuplicate = selectedImages.some(existingFile =>
existingFile.name === file.name && existingFile.size === file.size
const isDuplicate = selectedImages.some(
(existingFile) =>
existingFile.name === file.name && existingFile.size === file.size
);
if (!isDuplicate) {
selectedImages.push(file);
addedCount++;
@ -365,7 +410,7 @@ imageInput.addEventListener("change", function (event) {
alert(`Файл "${file.name}" не является изображением`);
}
});
if (addedCount > 0) {
updateImagePreview();
// Показываем уведомление о добавленных файлах
@ -375,9 +420,9 @@ imageInput.addEventListener("change", function (event) {
console.log(`Добавлено ${addedCount} изображений`);
}
}
// Очищаем input для возможности повторного выбора тех же файлов
event.target.value = '';
event.target.value = "";
});
// Обработчик очистки всех изображений
@ -420,19 +465,20 @@ function updateImagePreview() {
reader.onload = function (e) {
const previewItem = document.createElement("div");
previewItem.className = "image-preview-item";
// Форматируем размер файла
const fileSize = (file.size / 1024 / 1024).toFixed(2);
const fileName = file.name.length > 20 ? file.name.substring(0, 20) + '...' : file.name;
const fileName =
file.name.length > 20 ? file.name.substring(0, 20) + "..." : file.name;
previewItem.innerHTML = `
<img src="${e.target.result}" alt="Preview">
<img src="${e.target.result}" alt="Preview" loading="lazy">
<button class="remove-image-btn" data-index="${index}" title="Удалить изображение">×</button>
<div class="image-info">${fileName}<br>${fileSize} MB</div>
`;
imagePreviewList.appendChild(previewItem);
// Обработчик удаления изображения
const removeBtn = previewItem.querySelector(".remove-image-btn");
removeBtn.addEventListener("click", function (event) {
@ -441,7 +487,7 @@ function updateImagePreview() {
selectedImages.splice(index, 1);
updateImagePreview();
});
// Дополнительный обработчик для touch событий
removeBtn.addEventListener("touchend", function (event) {
event.preventDefault();
@ -450,25 +496,25 @@ function updateImagePreview() {
updateImagePreview();
});
};
reader.onerror = function() {
console.error('Ошибка чтения файла:', file.name);
reader.onerror = function () {
console.error("Ошибка чтения файла:", file.name);
alert(`Ошибка чтения файла: ${file.name}`);
};
reader.readAsDataURL(file);
});
}
// Функция для отображения изображения в модальном окне
function showImageModal(imageSrc) {
console.log('showImageModal called with:', imageSrc);
console.log("showImageModal called with:", imageSrc);
try {
modalImage.src = imageSrc;
imageModal.style.display = "block";
console.log('Modal opened successfully');
console.log("Modal opened successfully");
} catch (error) {
console.error('Error in showImageModal:', error);
console.error("Error in showImageModal:", error);
}
}
@ -479,20 +525,23 @@ async function uploadImages(noteId) {
}
const formData = new FormData();
selectedImages.forEach(file => {
selectedImages.forEach((file) => {
formData.append("images", file);
});
try {
// Показываем индикатор загрузки для мобильных устройств
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia('(max-width: 768px)').matches;
const isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia("(max-width: 768px)").matches;
if (isMobile) {
// Создаем простое уведомление о загрузке
const loadingDiv = document.createElement('div');
loadingDiv.id = 'mobile-upload-loading';
const loadingDiv = document.createElement("div");
loadingDiv.id = "mobile-upload-loading";
loadingDiv.style.cssText = `
position: fixed;
top: 50%;
@ -523,23 +572,23 @@ async function uploadImages(noteId) {
}
const result = await response.json();
// Удаляем индикатор загрузки
const loadingDiv = document.getElementById('mobile-upload-loading');
const loadingDiv = document.getElementById("mobile-upload-loading");
if (loadingDiv) {
loadingDiv.remove();
}
return result.images || [];
} catch (error) {
console.error("Ошибка загрузки изображений:", error);
// Удаляем индикатор загрузки в случае ошибки
const loadingDiv = document.getElementById('mobile-upload-loading');
const loadingDiv = document.getElementById("mobile-upload-loading");
if (loadingDiv) {
loadingDiv.remove();
}
// Показываем ошибку пользователю
alert(`Ошибка загрузки изображений: ${error.message}`);
return [];
@ -582,9 +631,9 @@ async function deleteNoteImage(noteId, imageId) {
async function loadNotes(forceReload = false) {
const now = Date.now();
const CACHE_DURATION = 30000; // 30 секунд кэширования
// Используем кэш, если он не устарел и не требуется принудительная перезагрузка
if (!forceReload && notesCache && (now - lastLoadTime) < CACHE_DURATION) {
if (!forceReload && notesCache && now - lastLoadTime < CACHE_DURATION) {
allNotes = notesCache;
await renderNotes(notesCache);
renderCalendar();
@ -772,18 +821,18 @@ async function renderNotes(notes) {
// Используем изображения, которые уже пришли с заметкой
const noteImages = Array.isArray(note.images) ? note.images : [];
let imagesHtml = "";
if (noteImages.length > 0) {
imagesHtml = '<div class="note-images-container">';
noteImages.forEach(image => {
imagesHtml += `
imagesHtml = '<div class="note-images-container">';
noteImages.forEach((image) => {
imagesHtml += `
<div class="note-image-item">
<img src="${image.file_path}" alt="${image.original_name}" class="note-image" data-image-src="${image.file_path}">
<img src="${image.file_path}" alt="${image.original_name}" class="note-image" data-image-src="${image.file_path}" loading="lazy">
<button class="remove-note-image-btn" data-note-id="${note.id}" data-image-id="${image.id}" title="Удалить изображение">×</button>
</div>
`;
});
imagesHtml += '</div>';
});
imagesHtml += "</div>";
}
const noteHtml = `
@ -818,36 +867,39 @@ async function renderNotes(notes) {
// Обрабатываем длинные заметки
handleLongNotes();
// Инициализируем lazy loading для новых изображений
initLazyLoading();
}
// Функция для обработки длинных заметок
function handleLongNotes() {
const MAX_HEIGHT = 300; // Максимальная высота в пикселях
document.querySelectorAll(".textNote").forEach((noteElement) => {
// Проверяем высоту контента
const contentHeight = noteElement.scrollHeight;
if (contentHeight > MAX_HEIGHT) {
// Добавляем класс для сворачивания
noteElement.classList.add("collapsed");
// Создаем кнопку "Показать все"
const showMoreBtn = document.createElement("button");
showMoreBtn.classList.add("show-more-btn");
showMoreBtn.textContent = "Показать полностью";
showMoreBtn.setAttribute("data-expanded", "false");
// Вставляем кнопку после заметки
noteElement.parentElement.insertBefore(
showMoreBtn,
noteElement.nextSibling
);
// Обработчик клика на кнопку
showMoreBtn.addEventListener("click", function () {
const isExpanded = this.getAttribute("data-expanded") === "true";
if (isExpanded) {
// Сворачиваем
noteElement.classList.add("collapsed");
@ -899,7 +951,7 @@ function addNoteEventListeners() {
// Разворачиваем заметку при редактировании
noteContent.classList.remove("collapsed");
// Скрываем кнопку "Показать полностью" если она есть
const showMoreBtn = noteContainer.querySelector(".show-more-btn");
if (showMoreBtn) {
@ -990,7 +1042,7 @@ function addNoteEventListeners() {
saveButtonContainer.appendChild(saveHint);
// Функция обновления превью изображений для режима редактирования
const updateEditImagePreview = function() {
const updateEditImagePreview = function () {
if (editSelectedImages.length === 0) {
editImagePreviewContainer.style.display = "none";
return;
@ -1004,15 +1056,15 @@ function addNoteEventListeners() {
reader.onload = function (e) {
const previewItem = document.createElement("div");
previewItem.className = "image-preview-item";
previewItem.innerHTML = `
<img src="${e.target.result}" alt="Preview">
<img src="${e.target.result}" alt="Preview" loading="lazy">
<button class="remove-image-btn" data-index="${index}">×</button>
<div class="image-info">${file.name}</div>
`;
editImagePreviewList.appendChild(previewItem);
// Обработчик удаления изображения
const removeBtn = previewItem.querySelector(".remove-image-btn");
removeBtn.addEventListener("click", function () {
@ -1025,13 +1077,13 @@ function addNoteEventListeners() {
};
// Функция загрузки изображений для режима редактирования
const uploadEditImages = async function(noteId) {
const uploadEditImages = async function (noteId) {
if (editSelectedImages.length === 0) {
return [];
}
const formData = new FormData();
editSelectedImages.forEach(file => {
editSelectedImages.forEach((file) => {
formData.append("images", file);
});
@ -1099,8 +1151,8 @@ function addNoteEventListeners() {
// Обработчики для загрузки изображений в режиме редактирования
editImageInput.addEventListener("change", function (event) {
const files = Array.from(event.target.files);
files.forEach(file => {
if (file.type.startsWith('image/')) {
files.forEach((file) => {
if (file.type.startsWith("image/")) {
editSelectedImages.push(file);
}
});
@ -1108,7 +1160,9 @@ function addNoteEventListeners() {
});
// Обработчик очистки всех изображений в режиме редактирования
const editClearImagesBtn = editImagePreviewHeader.querySelector(`#editClearImagesBtn-${noteId}`);
const editClearImagesBtn = editImagePreviewHeader.querySelector(
`#editClearImagesBtn-${noteId}`
);
editClearImagesBtn.addEventListener("click", function () {
editSelectedImages.length = 0;
updateEditImagePreview();
@ -1180,50 +1234,55 @@ function addImageEventListeners() {
if (imageElement._clickHandler) {
return; // Пропускаем, если обработчик уже добавлен
}
// Создаем новый обработчик
imageElement._clickHandler = function (event) {
event.preventDefault();
event.stopPropagation();
const imageSrc = this.dataset.imageSrc;
console.log('Image clicked, src:', imageSrc); // Для отладки
console.log("Image clicked, src:", imageSrc); // Для отладки
if (imageSrc) {
showImageModal(imageSrc);
}
};
imageElement.addEventListener("click", imageElement._clickHandler);
});
// Обработчики для кнопок удаления изображений
document.querySelectorAll(".remove-note-image-btn").forEach((buttonElement) => {
// Удаляем старые обработчики, если они есть
if (buttonElement._clickHandler) {
buttonElement.removeEventListener("click", buttonElement._clickHandler);
}
// Создаем новый обработчик
buttonElement._clickHandler = async function (event) {
event.preventDefault();
event.stopPropagation();
const noteId = this.dataset.noteId;
const imageId = this.dataset.imageId;
if (noteId && imageId && confirm("Вы уверены, что хотите удалить это изображение?")) {
const success = await deleteNoteImage(noteId, imageId);
if (success) {
await loadNotes(true); // Перезагружаем заметки
} else {
alert("Ошибка удаления изображения");
}
document
.querySelectorAll(".remove-note-image-btn")
.forEach((buttonElement) => {
// Удаляем старые обработчики, если они есть
if (buttonElement._clickHandler) {
buttonElement.removeEventListener("click", buttonElement._clickHandler);
}
};
buttonElement.addEventListener("click", buttonElement._clickHandler);
});
}
// Создаем новый обработчик
buttonElement._clickHandler = async function (event) {
event.preventDefault();
event.stopPropagation();
const noteId = this.dataset.noteId;
const imageId = this.dataset.imageId;
if (
noteId &&
imageId &&
confirm("Вы уверены, что хотите удалить это изображение?")
) {
const success = await deleteNoteImage(noteId, imageId);
if (success) {
await loadNotes(true); // Перезагружаем заметки
} else {
alert("Ошибка удаления изображения");
}
}
};
buttonElement.addEventListener("click", buttonElement._clickHandler);
});
}
// Функция сохранения заметки (вынесена отдельно для повторного использования)
async function saveNote() {
@ -1232,14 +1291,17 @@ async function saveNote() {
const { date, time } = getFormattedDateTime();
// Показываем индикатор сохранения для мобильных устройств
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia('(max-width: 768px)').matches;
const isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia("(max-width: 768px)").matches;
let savingIndicator = null;
if (isMobile) {
savingIndicator = document.createElement('div');
savingIndicator.id = 'mobile-saving-indicator';
savingIndicator = document.createElement("div");
savingIndicator.id = "mobile-saving-indicator";
savingIndicator.style.cssText = `
position: fixed;
top: 50%;
@ -1255,7 +1317,11 @@ async function saveNote() {
`;
savingIndicator.innerHTML = `
<div>💾 Сохранение заметки...</div>
${selectedImages.length > 0 ? `<div style="font-size: 12px; margin-top: 10px;">+ ${selectedImages.length} изображений</div>` : ''}
${
selectedImages.length > 0
? `<div style="font-size: 12px; margin-top: 10px;">+ ${selectedImages.length} изображений</div>`
: ""
}
`;
document.body.appendChild(savingIndicator);
}
@ -1296,10 +1362,10 @@ async function saveNote() {
updateImagePreview();
imageInput.value = "";
await loadNotes(true);
// Показываем уведомление об успешном сохранении
if (isMobile) {
const successDiv = document.createElement('div');
const successDiv = document.createElement("div");
successDiv.style.cssText = `
position: fixed;
top: 20px;
@ -1312,23 +1378,24 @@ async function saveNote() {
z-index: 10000;
font-size: 14px;
`;
successDiv.textContent = '✅ Заметка сохранена!';
successDiv.textContent = "✅ Заметка сохранена!";
document.body.appendChild(successDiv);
setTimeout(() => {
successDiv.remove();
}, 3000);
}
} catch (error) {
console.error("Ошибка:", error);
// Удаляем индикатор сохранения в случае ошибки
const savingIndicator = document.getElementById('mobile-saving-indicator');
const savingIndicator = document.getElementById(
"mobile-saving-indicator"
);
if (savingIndicator) {
savingIndicator.remove();
}
alert("Ошибка сохранения заметки");
}
}
@ -1352,7 +1419,10 @@ document.addEventListener("DOMContentLoaded", function () {
loadUserInfo();
loadNotes();
updateFilterIndicator();
// Инициализируем lazy loading для изображений
initLazyLoading();
// Добавляем обработчик для кнопки выхода
setupLogoutHandler();
});
@ -1360,41 +1430,41 @@ document.addEventListener("DOMContentLoaded", function () {
// Функция для настройки обработчика выхода
function setupLogoutHandler() {
const logoutForms = document.querySelectorAll('form[action="/logout"]');
logoutForms.forEach(form => {
form.addEventListener('submit', function(e) {
logoutForms.forEach((form) => {
form.addEventListener("submit", function (e) {
// Очищаем localStorage перед выходом
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('username');
localStorage.removeItem("isAuthenticated");
localStorage.removeItem("username");
});
});
}
// Функция для проверки аутентификации
async function checkAuthentication() {
const isAuthenticated = localStorage.getItem('isAuthenticated');
if (isAuthenticated !== 'true') {
const isAuthenticated = localStorage.getItem("isAuthenticated");
if (isAuthenticated !== "true") {
// Если пользователь не аутентифицирован, перенаправляем на страницу входа
window.location.href = "/";
return;
}
// Проверяем, что сессия на сервере еще действительна
try {
const response = await fetch("/api/auth/status");
if (!response.ok) {
// Если сессия недействительна, очищаем localStorage и перенаправляем
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('username');
localStorage.removeItem("isAuthenticated");
localStorage.removeItem("username");
window.location.href = "/";
return;
}
const authData = await response.json();
if (!authData.authenticated) {
// Если сервер говорит, что пользователь не аутентифицирован
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('username');
localStorage.removeItem("isAuthenticated");
localStorage.removeItem("username");
window.location.href = "/";
return;
}
@ -1432,6 +1502,16 @@ async function loadUserInfo() {
if (userAvatarContainer) {
userAvatarContainer.style.display = "none";
}
// Применяем цветовой акцент пользователя (только если отличается от текущего)
const currentColor = getComputedStyle(document.documentElement)
.getPropertyValue("--accent-color")
.trim();
const newColor = user.accent_color || "#007bff";
if (currentColor !== newColor) {
document.documentElement.style.setProperty("--accent-color", newColor);
}
}
} catch (error) {
console.error("Ошибка загрузки информации о пользователе:", error);
@ -1517,7 +1597,10 @@ function renderCalendar() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClick(event));
dayDiv.addEventListener(
"click",
async (event) => await handleDayClick(event)
);
calendarDays.appendChild(dayDiv);
}
@ -1554,7 +1637,10 @@ function renderCalendar() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClick(event));
dayDiv.addEventListener(
"click",
async (event) => await handleDayClick(event)
);
calendarDays.appendChild(dayDiv);
}
@ -1586,7 +1672,10 @@ function renderCalendar() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClick(event));
dayDiv.addEventListener(
"click",
async (event) => await handleDayClick(event)
);
calendarDays.appendChild(dayDiv);
}
@ -1827,7 +1916,10 @@ function renderCalendarMobile() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event));
dayDiv.addEventListener(
"click",
async (event) => await handleDayClickMobile(event)
);
calendarDays.appendChild(dayDiv);
}
@ -1864,7 +1956,10 @@ function renderCalendarMobile() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event));
dayDiv.addEventListener(
"click",
async (event) => await handleDayClickMobile(event)
);
calendarDays.appendChild(dayDiv);
}
@ -1896,7 +1991,10 @@ function renderCalendarMobile() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event));
dayDiv.addEventListener(
"click",
async (event) => await handleDayClickMobile(event)
);
calendarDays.appendChild(dayDiv);
}
@ -1946,7 +2044,10 @@ function renderTagsMobile() {
// Добавляем обработчики кликов для тегов
tagsContainer.querySelectorAll(".tag").forEach((tagElement) => {
tagElement.addEventListener("click", async (event) => await handleTagClickMobile(event));
tagElement.addEventListener(
"click",
async (event) => await handleTagClickMobile(event)
);
});
}

View File

@ -1,333 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Тест PWA для Brave - NoteJS</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
border-left: 4px solid;
}
.success {
background: #d4edda;
border-color: #28a745;
color: #155724;
}
.error {
background: #f8d7da;
border-color: #dc3545;
color: #721c24;
}
.warning {
background: #fff3cd;
border-color: #ffc107;
color: #856404;
}
.info {
background: #d1ecf1;
border-color: #17a2b8;
color: #0c5460;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.test-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.browser-info {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
}
</style>
</head>
<body>
<h1>🔍 Диагностика PWA для Brave браузера</h1>
<div class="browser-info">
<h3>Информация о браузере:</h3>
<div id="browserInfo"></div>
</div>
<div class="test-section">
<h2>📋 Проверка требований PWA</h2>
<div id="pwaRequirements"></div>
</div>
<div class="test-section">
<h2>🔧 Тестирование установки</h2>
<button id="installBtn" style="display: none">
Установить приложение
</button>
<button id="checkInstallBtn">Проверить возможность установки</button>
<div id="installStatus"></div>
</div>
<div class="test-section">
<h2>📱 Инструкции для Brave</h2>
<div id="braveInstructions"></div>
</div>
<div class="test-section">
<h2>🐛 Отладочная информация</h2>
<button id="debugBtn">Показать отладочную информацию</button>
<div id="debugInfo" style="display: none"></div>
</div>
<script>
let deferredPrompt;
let isBrave = false;
// Определяем браузер
function detectBrowser() {
const userAgent = navigator.userAgent;
isBrave =
userAgent.includes("Brave") ||
(navigator.brave && navigator.brave.isBrave);
const browserInfo = document.getElementById("browserInfo");
browserInfo.innerHTML = `
<p><strong>User Agent:</strong> ${userAgent}</p>
<p><strong>Браузер:</strong> ${isBrave ? "Brave" : "Другой"}</p>
<p><strong>Платформа:</strong> ${navigator.platform}</p>
<p><strong>Мобильное устройство:</strong> ${
isMobile() ? "Да" : "Нет"
}</p>
<p><strong>HTTPS:</strong> ${
location.protocol === "https:" ? "Да" : "Нет"
}</p>
<p><strong>Localhost:</strong> ${
location.hostname === "localhost" ? "Да" : "Нет"
}</p>
`;
}
function isMobile() {
return (
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia("(max-width: 768px)").matches
);
}
// Проверка требований PWA
function checkPWARequirements() {
const requirements = document.getElementById("pwaRequirements");
let html = "";
// HTTPS или localhost
const isSecure =
location.protocol === "https:" || location.hostname === "localhost";
html += `<div class="status ${isSecure ? "success" : "error"}">
${isSecure ? "✅" : "❌"} HTTPS или localhost: ${
isSecure ? "Да" : "Нет"
}
</div>`;
// Service Worker
const hasSW = "serviceWorker" in navigator;
html += `<div class="status ${hasSW ? "success" : "error"}">
${hasSW ? "✅" : "❌"} Service Worker API: ${
hasSW ? "Поддерживается" : "Не поддерживается"
}
</div>`;
// Manifest
fetch("/manifest.json")
.then((response) => response.ok)
.then((manifestOk) => {
html += `<div class="status ${manifestOk ? "success" : "error"}">
${manifestOk ? "✅" : "❌"} Manifest.json: ${
manifestOk ? "Загружается" : "Ошибка загрузки"
}
</div>`;
requirements.innerHTML = html;
})
.catch(() => {
html += `<div class="status error">
❌ Manifest.json: Ошибка загрузки
</div>`;
requirements.innerHTML = html;
});
// Иконки
fetch("/icons/icon-192x192.png")
.then((response) => response.ok)
.then((iconOk) => {
html += `<div class="status ${iconOk ? "success" : "error"}">
${iconOk ? "✅" : "❌"} Иконки: ${
iconOk ? "Доступны" : "Не найдены"
}
</div>`;
requirements.innerHTML = html;
})
.catch(() => {
html += `<div class="status error">
❌ Иконки: Не найдены
</div>`;
requirements.innerHTML = html;
});
}
// Проверка возможности установки
function checkInstallability() {
const status = document.getElementById("installStatus");
status.innerHTML =
'<div class="status info">Проверяем возможность установки...</div>';
// Проверяем, установлено ли уже приложение
if (
window.matchMedia("(display-mode: standalone)").matches ||
window.navigator.standalone === true
) {
status.innerHTML =
'<div class="status warning">⚠️ Приложение уже установлено</div>';
return;
}
// Проверяем beforeinstallprompt
if (deferredPrompt) {
status.innerHTML =
'<div class="status success">✅ Приложение можно установить</div>';
document.getElementById("installBtn").style.display = "inline-block";
} else {
status.innerHTML =
'<div class="status warning">⚠️ Событие beforeinstallprompt не сработало</div>';
}
}
// Инструкции для Brave
function showBraveInstructions() {
const instructions = document.getElementById("braveInstructions");
let html = "";
if (isBrave) {
html += `
<div class="status info">
<h4>Инструкции для Brave браузера:</h4>
<ol>
<li>Убедитесь, что у вас включены PWA в настройках Brave</li>
<li>Перейдите в <strong>Настройки → Дополнительно → Сайты и разрешения → PWA</strong></li>
<li>Убедитесь, что PWA включены</li>
<li>Попробуйте установить приложение через меню браузера</li>
</ol>
</div>
`;
}
html += `
<div class="status info">
<h4>Общие инструкции для установки PWA:</h4>
<ul>
<li><strong>Android:</strong> Нажмите на меню браузера (три точки) → "Установить приложение"</li>
<li><strong>iOS:</strong> Нажмите кнопку "Поделиться" → "На экран Домой"</li>
<li><strong>Desktop:</strong> Нажмите на иконку установки в адресной строке</li>
</ul>
</div>
`;
instructions.innerHTML = html;
}
// Отладочная информация
function showDebugInfo() {
const debugInfo = document.getElementById("debugInfo");
debugInfo.style.display =
debugInfo.style.display === "none" ? "block" : "none";
if (debugInfo.style.display === "block") {
debugInfo.innerHTML = `
<h4>Отладочная информация:</h4>
<pre>${JSON.stringify(
{
userAgent: navigator.userAgent,
isBrave: isBrave,
isMobile: isMobile(),
protocol: location.protocol,
hostname: location.hostname,
standalone: window.navigator.standalone,
displayMode: window.matchMedia(
"(display-mode: standalone)"
).matches,
serviceWorker: "serviceWorker" in navigator,
beforeinstallprompt: !!deferredPrompt,
},
null,
2
)}</pre>
`;
}
}
// Обработчики событий
window.addEventListener("beforeinstallprompt", (e) => {
console.log("beforeinstallprompt event fired");
e.preventDefault();
deferredPrompt = e;
checkInstallability();
});
window.addEventListener("appinstalled", () => {
console.log("PWA installed successfully");
document.getElementById("installStatus").innerHTML =
'<div class="status success">✅ Приложение успешно установлено!</div>';
});
// Инициализация
document.addEventListener("DOMContentLoaded", () => {
detectBrowser();
checkPWARequirements();
showBraveInstructions();
document
.getElementById("checkInstallBtn")
.addEventListener("click", checkInstallability);
document
.getElementById("installBtn")
.addEventListener("click", async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const choiceResult = await deferredPrompt.userChoice;
if (choiceResult.outcome === "accepted") {
console.log("User accepted the install prompt");
}
deferredPrompt = null;
}
});
document
.getElementById("debugBtn")
.addEventListener("click", showDebugInfo);
});
</script>
</body>
</html>

View File

@ -2,7 +2,10 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Вход в систему заметок</title>
<!-- PWA Meta Tags -->
@ -156,56 +159,6 @@
});
});
}
// Обработка установки PWA
let deferredPrompt;
window.addEventListener("beforeinstallprompt", (e) => {
console.log("beforeinstallprompt event fired");
// Определяем браузер
const isBrave = navigator.userAgent.includes('Brave') ||
(navigator.brave && await navigator.brave.isBrave());
const isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia("(max-width: 768px)").matches;
// Для Brave на мобильных устройствах не предотвращаем стандартное поведение
if (!isMobile && !isBrave) {
e.preventDefault();
}
deferredPrompt = e;
// Показываем кнопку установки на мобильных устройствах или в Brave
if (isMobile || isBrave) {
const installButton = document.createElement("button");
installButton.textContent = "Установить приложение";
installButton.className = "btnSave";
installButton.style.marginTop = "10px";
installButton.style.width = "100%";
installButton.addEventListener("click", async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const choiceResult = await deferredPrompt.userChoice;
if (choiceResult.outcome === "accepted") {
console.log("Пользователь установил приложение");
}
deferredPrompt = null;
installButton.remove();
}
});
document.querySelector(".auth-link").appendChild(installButton);
}
});
// Обработка успешной установки
window.addEventListener("appinstalled", () => {
console.log("PWA установлено успешно");
});
</script>
</body>
</html>

View File

@ -1,230 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Тест установки PWA на мобильном</title>
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/icon.svg" />
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.btn {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
margin: 10px 5px;
font-size: 16px;
}
.btn:hover {
background: #0056b3;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.info {
background: #e7f3ff;
border: 1px solid #b3d9ff;
padding: 15px;
border-radius: 5px;
margin: 15px 0;
}
.success {
background: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.warning {
background: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.error {
background: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.status {
margin: 10px 0;
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>📱 Тест установки PWA на мобильном</h1>
<div class="info">
<h3>Инструкции:</h3>
<p>1. Откройте эту страницу на мобильном устройстве</p>
<p>2. Должен появиться нативный баннер установки браузера</p>
<p>3. Если баннер не появился, используйте кнопки ниже</p>
</div>
<div id="status" class="status"></div>
<div>
<button id="install-btn" class="btn" disabled>
Установить приложение
</button>
<button id="show-banner-btn" class="btn">Показать баннер</button>
<button id="check-status-btn" class="btn">Проверить статус</button>
</div>
<div id="debug-info" class="info" style="margin-top: 20px">
<h3>Отладочная информация:</h3>
<div id="debug-content"></div>
</div>
</div>
<script>
let deferredPrompt;
const statusDiv = document.getElementById("status");
const installBtn = document.getElementById("install-btn");
const showBannerBtn = document.getElementById("show-banner-btn");
const checkStatusBtn = document.getElementById("check-status-btn");
const debugContent = document.getElementById("debug-content");
function updateStatus(message, type = "info") {
statusDiv.textContent = message;
statusDiv.className = `status ${type}`;
}
function updateDebugInfo() {
const info = {
userAgent: navigator.userAgent,
isMobile:
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
),
hasDeferredPrompt: !!deferredPrompt,
isPWAInstalled: window.matchMedia("(display-mode: standalone)")
.matches,
isOnline: navigator.onLine,
hasServiceWorker: "serviceWorker" in navigator,
};
debugContent.innerHTML = Object.entries(info)
.map(([key, value]) => `<strong>${key}:</strong> ${value}`)
.join("<br>");
}
// Обработка события beforeinstallprompt
window.addEventListener("beforeinstallprompt", (e) => {
console.log("beforeinstallprompt событие получено");
// На мобильных устройствах позволяем браузеру показать нативный баннер
const isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia("(max-width: 768px)").matches;
if (!isMobile) {
e.preventDefault();
updateStatus(
"Десктоп: preventDefault() вызван, показываем кастомную кнопку",
"warning"
);
} else {
updateStatus(
"Мобильное устройство: разрешаем нативный баннер установки",
"success"
);
}
deferredPrompt = e;
installBtn.disabled = false;
updateDebugInfo();
});
// Обработка успешной установки
window.addEventListener("appinstalled", () => {
console.log("PWA установлено успешно");
updateStatus("Приложение установлено успешно!", "success");
installBtn.disabled = true;
deferredPrompt = null;
updateDebugInfo();
});
// Кнопка установки
installBtn.addEventListener("click", async () => {
if (deferredPrompt) {
updateStatus("Показываем баннер установки...", "info");
deferredPrompt.prompt();
const choiceResult = await deferredPrompt.userChoice;
if (choiceResult.outcome === "accepted") {
updateStatus("Пользователь установил приложение!", "success");
} else {
updateStatus("Пользователь отклонил установку", "warning");
}
deferredPrompt = null;
installBtn.disabled = true;
} else {
updateStatus("Баннер установки недоступен", "error");
}
updateDebugInfo();
});
// Кнопка показа баннера
showBannerBtn.addEventListener("click", () => {
if (deferredPrompt) {
updateStatus("Принудительно показываем баннер...", "info");
deferredPrompt.prompt();
} else {
updateStatus(
"Баннер установки недоступен. Попробуйте перезагрузить страницу.",
"error"
);
}
});
// Кнопка проверки статуса
checkStatusBtn.addEventListener("click", () => {
updateDebugInfo();
if (deferredPrompt) {
updateStatus("Баннер установки доступен", "success");
} else {
updateStatus("Баннер установки недоступен", "warning");
}
});
// Инициализация
updateDebugInfo();
updateStatus("Ожидание события beforeinstallprompt...", "info");
// Регистрация Service Worker
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("SW зарегистрирован:", registration);
updateStatus("Service Worker зарегистрирован", "success");
})
.catch((error) => {
console.log("Ошибка регистрации SW:", error);
updateStatus("Ошибка регистрации Service Worker", "error");
});
}
</script>
</body>
</html>

View File

@ -1,415 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Тест PWA для мобильных устройств - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="Тестирование PWA на мобильных устройствах" />
<meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="msapplication-TileColor" content="#007bff" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
<meta name="application-name" content="NoteJS" />
<meta name="format-detection" content="telephone=no" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/icons/icon-128x128.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/icons/icon-128x128.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css" />
<style>
.test-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.test-section {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.test-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #e9ecef;
}
.test-item:last-child {
border-bottom: none;
}
.status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.status.pass {
background: #d4edda;
color: #155724;
}
.status.fail {
background: #f8d7da;
color: #721c24;
}
.status.warning {
background: #fff3cd;
color: #856404;
}
.install-button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
width: 100%;
margin: 10px 0;
}
.install-button:hover {
background: #0056b3;
}
.install-button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.instructions {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 6px;
padding: 15px;
margin: 15px 0;
}
.instructions h4 {
margin-top: 0;
color: #0066cc;
}
.instructions ol {
margin: 10px 0;
padding-left: 20px;
}
.instructions li {
margin: 5px 0;
}
.debug-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
}
</style>
</head>
<body>
<div class="test-container">
<h1>📱 Тест PWA для мобильных устройств</h1>
<div class="test-section">
<h3>Проверка требований PWA</h3>
<div id="pwa-checks">
<div class="test-item">
<span>HTTPS или localhost</span>
<span class="status" id="https-check">Проверка...</span>
</div>
<div class="test-item">
<span>Service Worker</span>
<span class="status" id="sw-check">Проверка...</span>
</div>
<div class="test-item">
<span>Manifest</span>
<span class="status" id="manifest-check">Проверка...</span>
</div>
<div class="test-item">
<span>Иконки</span>
<span class="status" id="icons-check">Проверка...</span>
</div>
<div class="test-item">
<span>Уже установлено</span>
<span class="status" id="installed-check">Проверка...</span>
</div>
</div>
</div>
<div class="test-section">
<h3>Установка приложения</h3>
<div id="device-info" style="margin-bottom: 10px; padding: 10px; background: #e7f3ff; border-radius: 4px; font-size: 14px;"></div>
<button id="install-button" class="install-button" disabled>
Установить приложение
</button>
<div id="install-status"></div>
</div>
<div class="test-section">
<h3>Инструкции по установке</h3>
<div class="instructions">
<h4>Android Chrome/Edge:</h4>
<ol>
<li>Откройте сайт в Chrome или Edge</li>
<li>Нажмите кнопку "Установить приложение" выше</li>
<li>Или нажмите меню (⋮) → "Установить приложение"</li>
<li>Следуйте инструкциям браузера</li>
</ol>
</div>
<div class="instructions">
<h4>iOS Safari:</h4>
<ol>
<li>Откройте сайт в Safari</li>
<li>Нажмите кнопку "Поделиться" (□↗)</li>
<li>Выберите "На экран Домой"</li>
<li>Нажмите "Добавить"</li>
</ol>
</div>
<div class="instructions">
<h4>Другие браузеры:</h4>
<ol>
<li>Найдите опцию "Установить" в меню браузера</li>
<li>Или используйте "Добавить на главный экран"</li>
</ol>
</div>
</div>
<div class="test-section">
<h3>Отладочная информация</h3>
<button onclick="showDebugInfo()" class="install-button">Показать отладочную информацию</button>
<div id="debug-info" class="debug-info" style="display: none;"></div>
</div>
<div class="test-section">
<h3>Действия</h3>
<button onclick="clearCache()" class="install-button">Очистить кэш</button>
<button onclick="forceUpdate()" class="install-button">Принудительное обновление</button>
<button onclick="location.reload()" class="install-button">Перезагрузить страницу</button>
</div>
</div>
<script>
let deferredPrompt;
// Проверка требований PWA
function checkPWARequirements() {
// HTTPS или localhost
const isSecure = location.protocol === 'https:' || location.hostname === 'localhost';
document.getElementById('https-check').textContent = isSecure ? '✅ Да' : '❌ Нет';
document.getElementById('https-check').className = `status ${isSecure ? 'pass' : 'fail'}`;
// Service Worker
const hasSW = 'serviceWorker' in navigator;
document.getElementById('sw-check').textContent = hasSW ? '✅ Да' : '❌ Нет';
document.getElementById('sw-check').className = `status ${hasSW ? 'pass' : 'fail'}`;
// Manifest
const hasManifest = document.querySelector('link[rel="manifest"]') !== null;
document.getElementById('manifest-check').textContent = hasManifest ? '✅ Да' : '❌ Нет';
document.getElementById('manifest-check').className = `status ${hasManifest ? 'pass' : 'fail'}`;
// Иконки
const hasIcons = document.querySelector('link[rel="icon"]') !== null;
document.getElementById('icons-check').textContent = hasIcons ? '✅ Да' : '❌ Нет';
document.getElementById('icons-check').className = `status ${hasIcons ? 'pass' : 'fail'}`;
// Уже установлено
const isInstalled = window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
document.getElementById('installed-check').textContent = isInstalled ? '✅ Да' : '❌ Нет';
document.getElementById('installed-check').className = `status ${isInstalled ? 'warning' : 'pass'}`;
}
// Регистрация Service Worker
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW зарегистрирован успешно:', registration.scope);
})
.catch((error) => {
console.log('Ошибка регистрации SW:', error);
});
}
}
// Обработка установки
window.addEventListener('beforeinstallprompt', (e) => {
console.log('beforeinstallprompt событие получено');
e.preventDefault();
deferredPrompt = e;
// Проверяем, является ли устройство мобильным
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia('(max-width: 768px)').matches;
const installButton = document.getElementById('install-button');
if (isMobile) {
installButton.disabled = false;
installButton.textContent = '📱 Установить приложение';
} else {
installButton.disabled = true;
installButton.textContent = '💻 Установка доступна через меню браузера';
}
});
// Обработка успешной установки
window.addEventListener('appinstalled', () => {
console.log('PWA установлено успешно');
document.getElementById('install-status').innerHTML =
'<div style="color: green; margin-top: 10px;">✅ Приложение успешно установлено!</div>';
const installButton = document.getElementById('install-button');
installButton.disabled = true;
installButton.textContent = 'Приложение установлено';
});
// Установка приложения
document.getElementById('install-button').addEventListener('click', () => {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
console.log('Результат установки:', choiceResult.outcome);
if (choiceResult.outcome === 'accepted') {
document.getElementById('install-status').innerHTML =
'<div style="color: green; margin-top: 10px;">✅ Пользователь принял установку</div>';
} else {
document.getElementById('install-status').innerHTML =
'<div style="color: orange; margin-top: 10px;">⚠️ Пользователь отклонил установку</div>';
}
deferredPrompt = null;
});
} else {
document.getElementById('install-status').innerHTML =
'<div style="color: red; margin-top: 10px;">❌ Установка недоступна. Попробуйте использовать меню браузера.</div>';
}
});
// Показать отладочную информацию
function showDebugInfo() {
const debugInfo = document.getElementById('debug-info');
const info = {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
onLine: navigator.onLine,
cookieEnabled: navigator.cookieEnabled,
displayMode: window.matchMedia('(display-mode: standalone)').matches ? 'standalone' : 'browser',
standalone: window.navigator.standalone,
hasServiceWorker: 'serviceWorker' in navigator,
hasDeferredPrompt: deferredPrompt !== null,
location: {
href: location.href,
protocol: location.protocol,
hostname: location.hostname,
port: location.port
}
};
debugInfo.textContent = JSON.stringify(info, null, 2);
debugInfo.style.display = debugInfo.style.display === 'none' ? 'block' : 'none';
}
// Очистка кэша
async function clearCache() {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
try {
navigator.serviceWorker.controller.postMessage({
type: 'CLEAR_ALL_CACHE'
});
alert('Запрос на очистку кэша отправлен');
} catch (error) {
alert('Ошибка при очистке кэша: ' + error.message);
}
} else {
alert('Service Worker не доступен');
}
}
// Принудительное обновление
async function forceUpdate() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
await registration.update();
alert('Проверка обновлений завершена');
}
} catch (error) {
alert('Ошибка при проверке обновлений: ' + error.message);
}
} else {
alert('Service Worker не поддерживается');
}
}
// Проверка типа устройства
function checkDeviceType() {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia('(max-width: 768px)').matches;
const deviceInfo = document.getElementById('device-info');
if (isMobile) {
deviceInfo.innerHTML = '📱 <strong>Мобильное устройство</strong> - кнопка установки доступна';
deviceInfo.style.background = '#d4edda';
deviceInfo.style.color = '#155724';
} else {
deviceInfo.innerHTML = '💻 <strong>ПК/Десктоп</strong> - кнопка установки скрыта (PWA доступно через меню браузера)';
deviceInfo.style.background = '#fff3cd';
deviceInfo.style.color = '#856404';
}
}
// Инициализация
document.addEventListener('DOMContentLoaded', () => {
checkDeviceType();
checkPWARequirements();
registerServiceWorker();
});
</script>
</body>
</html>

View File

@ -2,15 +2,24 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Заметки - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" />
<meta
name="description"
content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря"
/>
<meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="msapplication-TileColor" content="#007bff" />
@ -18,28 +27,58 @@
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
<meta name="application-name" content="NoteJS" />
<meta name="format-detection" content="telephone=no" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/icons/icon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/icons/icon-16x16.png"
/>
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/icons/icon-128x128.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/icons/icon-128x128.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<link
rel="apple-touch-icon"
sizes="114x114"
href="/icons/icon-128x128.png"
/>
<link
rel="apple-touch-icon"
sizes="120x120"
href="/icons/icon-128x128.png"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="/icons/icon-144x144.png"
/>
<link
rel="apple-touch-icon"
sizes="152x152"
href="/icons/icon-152x152.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/icons/icon-192x192.png"
/>
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css?v=4" />
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
@ -192,6 +231,7 @@
id="user-avatar"
src=""
alt="Аватар"
loading="lazy"
style="
width: 32px;
height: 32px;
@ -233,7 +273,11 @@
<button class="btnMarkdown" id="linkBtn" title="Ссылка">
<span class="iconify" data-icon="mdi:link"></span>
</button>
<button class="btnMarkdown" id="imageBtn" title="Загрузить изображения">
<button
class="btnMarkdown"
id="imageBtn"
title="Загрузить изображения"
>
<span class="iconify" data-icon="mdi:image-plus"></span>
</button>
</div>
@ -243,19 +287,35 @@
id="noteInput"
placeholder="Ваша заметка..."
></textarea>
<!-- Скрытый input для загрузки изображений -->
<input type="file" id="imageInput" accept="image/*" multiple style="display: none;">
<input
type="file"
id="imageInput"
accept="image/*"
multiple
style="display: none"
/>
<!-- Контейнер для отображения загруженных изображений -->
<div id="imagePreviewContainer" class="image-preview-container" style="display: none;">
<div
id="imagePreviewContainer"
class="image-preview-container"
style="display: none"
>
<div class="image-preview-header">
<span>Загруженные изображения:</span>
<button type="button" id="clearImagesBtn" class="clear-images-btn">Очистить все</button>
<button
type="button"
id="clearImagesBtn"
class="clear-images-btn"
>
Очистить все
</button>
</div>
<div id="imagePreviewList" class="image-preview-list"></div>
</div>
<div class="save-button-container">
<button class="btnSave" id="saveBtn">Сохранить</button>
<span class="save-hint">или нажмите Alt + Enter</span>
@ -272,14 +332,14 @@
<!-- Модальное окно для просмотра изображений -->
<div id="imageModal" class="image-modal">
<span class="image-modal-close">&times;</span>
<img class="image-modal-content" id="modalImage">
<img class="image-modal-content" id="modalImage" loading="lazy" />
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.0/marked.min.js"></script>
<script src="/app.js"></script>
<!-- PWA Script -->
<script src="/pwa.js"></script>
<script>
// Проверяем загрузку Iconify
document.addEventListener("DOMContentLoaded", function () {

View File

@ -2,31 +2,51 @@
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Личный кабинет - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" />
<meta
name="description"
content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря"
/>
<meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="msapplication-TileColor" content="#007bff" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/icons/icon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/icons/icon-16x16.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/icons/icon-192x192.png"
/>
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css?v=3" />
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
@ -56,6 +76,7 @@
src=""
alt="Аватар"
class="avatar-preview"
loading="lazy"
style="display: none"
/>
<div id="avatarPlaceholder" class="avatar-placeholder">
@ -105,6 +126,54 @@
<input type="email" id="email" placeholder="example@example.com" />
</div>
<div class="form-group">
<label for="accentColor">Цветовой акцент</label>
<div class="accent-color-picker">
<div
class="color-option"
data-color="#007bff"
style="background-color: #007bff"
title="Синий"
></div>
<div
class="color-option"
data-color="#28a745"
style="background-color: #28a745"
title="Зеленый"
></div>
<div
class="color-option"
data-color="#dc3545"
style="background-color: #dc3545"
title="Красный"
></div>
<div
class="color-option"
data-color="#fd7e14"
style="background-color: #fd7e14"
title="Оранжевый"
></div>
<div
class="color-option"
data-color="#6f42c1"
style="background-color: #6f42c1"
title="Фиолетовый"
></div>
<div
class="color-option"
data-color="#e83e8c"
style="background-color: #e83e8c"
title="Розовый"
></div>
</div>
<input
type="color"
id="accentColor"
value="#007bff"
style="margin-top: 10px"
/>
</div>
<button id="updateProfileBtn" class="btnSave">
Сохранить изменения
</button>
@ -155,7 +224,7 @@
</div>
<script src="/profile.js"></script>
<!-- PWA Script -->
<script src="/pwa.js"></script>
</body>

View File

@ -11,6 +11,48 @@ const newPasswordInput = document.getElementById("newPassword");
const confirmPasswordInput = document.getElementById("confirmPassword");
const changePasswordBtn = document.getElementById("changePasswordBtn");
const messageContainer = document.getElementById("messageContainer");
const accentColorInput = document.getElementById("accentColor");
// Lazy loading для изображений
function initLazyLoading() {
// Проверяем поддержку Intersection Observer API
if ("IntersectionObserver" in window) {
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
// Если у изображения есть data-src, загружаем его
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute("data-src");
}
img.classList.remove("lazy");
observer.unobserve(img);
}
});
},
{
rootMargin: "50px 0px",
threshold: 0.01,
}
);
// Наблюдаем за всеми изображениями с классом lazy
document.querySelectorAll("img.lazy").forEach((img) => {
imageObserver.observe(img);
});
} else {
// Fallback для старых браузеров
document.querySelectorAll("img.lazy").forEach((img) => {
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute("data-src");
}
img.classList.remove("lazy");
});
}
}
// Функция для показа сообщений
function showMessage(message, type = "success") {
@ -36,6 +78,14 @@ async function loadProfile() {
// Заполняем поля
usernameInput.value = user.username || "";
emailInput.value = user.email || "";
accentColorInput.value = user.accent_color || "#007bff";
// Обновляем выбранный цвет в цветовых опциях
updateColorPickerSelection(user.accent_color || "#007bff");
// Применяем цветовой акцент пользователя
const accentColor = user.accent_color || "#007bff";
document.documentElement.style.setProperty("--accent-color", accentColor);
// Обрабатываем аватарку
if (user.avatar) {
@ -54,6 +104,17 @@ async function loadProfile() {
}
}
// Функция для обновления выбора цвета в цветовой палитре
function updateColorPickerSelection(selectedColor) {
const colorOptions = document.querySelectorAll(".color-option");
colorOptions.forEach((option) => {
option.classList.remove("selected");
if (option.dataset.color === selectedColor) {
option.classList.add("selected");
}
});
}
// Обработчик загрузки аватарки
avatarInput.addEventListener("change", async function (event) {
const file = event.target.files[0];
@ -139,6 +200,7 @@ deleteAvatarBtn.addEventListener("click", async function () {
updateProfileBtn.addEventListener("click", async function () {
const username = usernameInput.value.trim();
const email = emailInput.value.trim();
const accentColor = accentColorInput.value;
// Валидация
if (!username) {
@ -165,6 +227,7 @@ updateProfileBtn.addEventListener("click", async function () {
body: JSON.stringify({
username,
email: email || null,
accent_color: accentColor,
}),
});
@ -174,6 +237,10 @@ updateProfileBtn.addEventListener("click", async function () {
}
const result = await response.json();
// Применяем новый цветовой акцент
document.documentElement.style.setProperty("--accent-color", accentColor);
showMessage(result.message || "Профиль успешно обновлен", "success");
} catch (error) {
console.error("Ошибка обновления профиля:", error);
@ -247,30 +314,30 @@ function isValidEmail(email) {
// Функция для проверки аутентификации
async function checkAuthentication() {
const isAuthenticated = localStorage.getItem('isAuthenticated');
if (isAuthenticated !== 'true') {
const isAuthenticated = localStorage.getItem("isAuthenticated");
if (isAuthenticated !== "true") {
// Если пользователь не аутентифицирован, перенаправляем на страницу входа
window.location.href = "/";
return;
}
// Проверяем, что сессия на сервере еще действительна
try {
const response = await fetch("/api/auth/status");
if (!response.ok) {
// Если сессия недействительна, очищаем localStorage и перенаправляем
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('username');
localStorage.removeItem("isAuthenticated");
localStorage.removeItem("username");
window.location.href = "/";
return;
}
const authData = await response.json();
if (!authData.authenticated) {
// Если сервер говорит, что пользователь не аутентифицирован
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('username');
localStorage.removeItem("isAuthenticated");
localStorage.removeItem("username");
window.location.href = "/";
return;
}
@ -285,21 +352,44 @@ async function checkAuthentication() {
// Функция для настройки обработчика выхода
function setupLogoutHandler() {
const logoutForms = document.querySelectorAll('form[action="/logout"]');
logoutForms.forEach(form => {
form.addEventListener('submit', function(e) {
logoutForms.forEach((form) => {
form.addEventListener("submit", function (e) {
// Очищаем localStorage перед выходом
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('username');
localStorage.removeItem("isAuthenticated");
localStorage.removeItem("username");
});
});
}
// Обработчики для цветовой палитры
function setupColorPicker() {
const colorOptions = document.querySelectorAll(".color-option");
colorOptions.forEach((option) => {
option.addEventListener("click", function () {
const selectedColor = this.dataset.color;
accentColorInput.value = selectedColor;
updateColorPickerSelection(selectedColor);
});
});
// Обработчик для input color
accentColorInput.addEventListener("input", function () {
updateColorPickerSelection(this.value);
});
}
// Загружаем профиль при загрузке страницы
document.addEventListener("DOMContentLoaded", function () {
// Проверяем аутентификацию при загрузке страницы
checkAuthentication();
loadProfile();
// Инициализируем lazy loading для изображений
initLazyLoading();
// Настраиваем цветовую палитру
setupColorPicker();
// Добавляем обработчик для кнопки выхода
setupLogoutHandler();
});

View File

@ -1,411 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Диагностика PWA - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="Диагностика PWA для NoteJS" />
<meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="msapplication-TileColor" content="#007bff" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
<meta name="application-name" content="NoteJS" />
<meta name="format-detection" content="telephone=no" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/icons/icon-128x128.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/icons/icon-128x128.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css" />
<style>
.debug-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.debug-section {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.debug-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #e9ecef;
}
.debug-item:last-child {
border-bottom: none;
}
.status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.status.pass {
background: #d4edda;
color: #155724;
}
.status.fail {
background: #f8d7da;
color: #721c24;
}
.status.warning {
background: #fff3cd;
color: #856404;
}
.debug-code {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
margin: 10px 0;
}
.install-button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
width: 100%;
margin: 10px 0;
}
.install-button:hover {
background: #0056b3;
}
.install-button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.success-message {
background: #d4edda;
color: #155724;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="debug-container">
<h1>🔍 Диагностика PWA</h1>
<div class="debug-section">
<h3>Основные требования PWA</h3>
<div id="basic-checks">
<div class="debug-item">
<span>HTTPS или localhost</span>
<span class="status" id="https-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Service Worker</span>
<span class="status" id="sw-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Manifest</span>
<span class="status" id="manifest-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Иконки</span>
<span class="status" id="icons-check">Проверка...</span>
</div>
</div>
</div>
<div class="debug-section">
<h3>Детальная диагностика</h3>
<div id="detailed-checks">
<div class="debug-item">
<span>Manifest загружается</span>
<span class="status" id="manifest-load-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Service Worker регистрируется</span>
<span class="status" id="sw-register-check">Проверка...</span>
</div>
<div class="debug-item">
<span>Иконки доступны</span>
<span class="status" id="icons-available-check">Проверка...</span>
</div>
<div class="debug-item">
<span>beforeinstallprompt событие</span>
<span class="status" id="install-prompt-check">Проверка...</span>
</div>
</div>
</div>
<div class="debug-section">
<h3>Установка приложения</h3>
<button id="install-button" class="install-button" disabled>
Установить приложение
</button>
<div id="install-status"></div>
</div>
<div class="debug-section">
<h3>Информация о манифесте</h3>
<div id="manifest-info" class="debug-code">Загрузка...</div>
</div>
<div class="debug-section">
<h3>Информация о Service Worker</h3>
<div id="sw-info" class="debug-code">Загрузка...</div>
</div>
<div class="debug-section">
<h3>Информация о браузере</h3>
<div id="browser-info" class="debug-code">Загрузка...</div>
</div>
<div class="debug-section">
<h3>Ошибки консоли</h3>
<div id="console-errors" class="debug-code">Нет ошибок</div>
</div>
</div>
<script>
let deferredPrompt;
let consoleErrors = [];
// Перехватываем ошибки консоли
const originalError = console.error;
console.error = function(...args) {
consoleErrors.push(args.join(' '));
originalError.apply(console, args);
};
// Проверка основных требований PWA
function checkBasicRequirements() {
// HTTPS или localhost
const isSecure = location.protocol === 'https:' || location.hostname === 'localhost';
document.getElementById('https-check').textContent = isSecure ? '✅ Да' : '❌ Нет';
document.getElementById('https-check').className = `status ${isSecure ? 'pass' : 'fail'}`;
// Service Worker
const hasSW = 'serviceWorker' in navigator;
document.getElementById('sw-check').textContent = hasSW ? '✅ Да' : '❌ Нет';
document.getElementById('sw-check').className = `status ${hasSW ? 'pass' : 'fail'}`;
// Manifest
const hasManifest = document.querySelector('link[rel="manifest"]') !== null;
document.getElementById('manifest-check').textContent = hasManifest ? '✅ Да' : '❌ Нет';
document.getElementById('manifest-check').className = `status ${hasManifest ? 'pass' : 'fail'}`;
// Иконки
const hasIcons = document.querySelector('link[rel="icon"]') !== null;
document.getElementById('icons-check').textContent = hasIcons ? '✅ Да' : '❌ Нет';
document.getElementById('icons-check').className = `status ${hasIcons ? 'pass' : 'fail'}`;
}
// Детальная диагностика
async function detailedDiagnostics() {
// Проверка загрузки манифеста
try {
const manifestResponse = await fetch('/manifest.json');
const manifest = await manifestResponse.json();
document.getElementById('manifest-load-check').textContent = '✅ Загружен';
document.getElementById('manifest-load-check').className = 'status pass';
// Отображаем информацию о манифесте
document.getElementById('manifest-info').textContent = JSON.stringify(manifest, null, 2);
} catch (error) {
document.getElementById('manifest-load-check').textContent = '❌ Ошибка загрузки';
document.getElementById('manifest-load-check').className = 'status fail';
document.getElementById('manifest-info').textContent = `Ошибка: ${error.message}`;
}
// Проверка регистрации Service Worker
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
document.getElementById('sw-register-check').textContent = '✅ Зарегистрирован';
document.getElementById('sw-register-check').className = 'status pass';
// Отображаем информацию о Service Worker
document.getElementById('sw-info').textContent = JSON.stringify({
scope: registration.scope,
state: registration.active ? registration.active.state : 'не активен',
scriptURL: registration.active ? registration.active.scriptURL : 'не доступен'
}, null, 2);
} else {
document.getElementById('sw-register-check').textContent = '❌ Не зарегистрирован';
document.getElementById('sw-register-check').className = 'status fail';
}
} catch (error) {
document.getElementById('sw-register-check').textContent = '❌ Ошибка';
document.getElementById('sw-register-check').className = 'status fail';
}
}
// Проверка доступности иконок
const iconSizes = ['192x192', '512x512'];
let iconsAvailable = 0;
for (const size of iconSizes) {
try {
const response = await fetch(`/icons/icon-${size}.png`);
if (response.ok) {
iconsAvailable++;
}
} catch (error) {
console.error(`Ошибка загрузки иконки ${size}:`, error);
}
}
if (iconsAvailable === iconSizes.length) {
document.getElementById('icons-available-check').textContent = '✅ Все доступны';
document.getElementById('icons-available-check').className = 'status pass';
} else {
document.getElementById('icons-available-check').textContent = `⚠️ ${iconsAvailable}/${iconSizes.length} доступны`;
document.getElementById('icons-available-check').className = 'status warning';
}
// Информация о браузере
document.getElementById('browser-info').textContent = JSON.stringify({
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
onLine: navigator.onLine,
cookieEnabled: navigator.cookieEnabled,
maxTouchPoints: navigator.maxTouchPoints,
displayMode: window.matchMedia('(display-mode: standalone)').matches ? 'standalone' : 'browser',
standalone: window.navigator.standalone
}, null, 2);
}
// Регистрация Service Worker
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW зарегистрирован успешно:', registration.scope);
})
.catch((error) => {
console.log('Ошибка регистрации SW:', error);
});
}
}
// Обработка установки
window.addEventListener('beforeinstallprompt', (e) => {
console.log('beforeinstallprompt событие получено');
e.preventDefault();
deferredPrompt = e;
document.getElementById('install-prompt-check').textContent = '✅ Получено';
document.getElementById('install-prompt-check').className = 'status pass';
const installButton = document.getElementById('install-button');
installButton.disabled = false;
installButton.textContent = '📱 Установить приложение';
});
// Обработка успешной установки
window.addEventListener('appinstalled', () => {
console.log('PWA установлено успешно');
document.getElementById('install-status').innerHTML =
'<div class="success-message">✅ Приложение успешно установлено!</div>';
const installButton = document.getElementById('install-button');
installButton.disabled = true;
installButton.textContent = 'Приложение установлено';
});
// Установка приложения
document.getElementById('install-button').addEventListener('click', () => {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
console.log('Результат установки:', choiceResult.outcome);
if (choiceResult.outcome === 'accepted') {
document.getElementById('install-status').innerHTML =
'<div class="success-message">✅ Пользователь принял установку</div>';
} else {
document.getElementById('install-status').innerHTML =
'<div class="error-message">⚠️ Пользователь отклонил установку</div>';
}
deferredPrompt = null;
});
} else {
document.getElementById('install-status').innerHTML =
'<div class="error-message">❌ Установка недоступна. Попробуйте использовать меню браузера.</div>';
}
});
// Обновление ошибок консоли
function updateConsoleErrors() {
if (consoleErrors.length > 0) {
document.getElementById('console-errors').textContent = consoleErrors.join('\n');
} else {
document.getElementById('console-errors').textContent = 'Нет ошибок';
}
}
// Инициализация
document.addEventListener('DOMContentLoaded', () => {
checkBasicRequirements();
detailedDiagnostics();
registerServiceWorker();
// Обновляем ошибки каждые 2 секунды
setInterval(updateConsoleErrors, 2000);
});
</script>
</body>
</html>

View File

@ -1,15 +1,12 @@
// PWA Service Worker Registration и установка
// PWA Service Worker Registration
class PWAManager {
constructor() {
this.deferredPrompt = null;
this.init();
}
init() {
console.log("PWA Manager инициализирован");
this.registerServiceWorker();
this.setupInstallPrompt();
this.setupAppInstalled();
this.checkPWARequirements();
this.setupServiceWorkerMessages();
}
@ -75,60 +72,6 @@ class PWAManager {
}
}
// Настройка промпта установки
setupInstallPrompt() {
window.addEventListener("beforeinstallprompt", (e) => {
console.log("beforeinstallprompt событие получено");
// На мобильных устройствах позволяем браузеру показать нативный баннер
if (this.isMobileDevice()) {
console.log(
"Мобильное устройство - разрешаем нативный баннер установки"
);
// Не вызываем preventDefault() для мобильных устройств
this.deferredPrompt = e;
return;
}
// На десктопе показываем кастомную кнопку
e.preventDefault();
this.deferredPrompt = e;
// Показываем кнопку установки с задержкой для лучшего UX
setTimeout(() => {
this.showInstallButton();
}, 1000);
});
}
// Показ кнопки установки
showInstallButton() {
console.log("Попытка показать кнопку установки");
// Проверяем, не установлено ли уже приложение
if (this.isPWAInstalled()) {
console.log("Приложение уже установлено");
return;
}
// Показываем кнопку только на десктопе (на мобильных используем нативный баннер)
if (this.isMobileDevice()) {
console.log(
"Мобильное устройство - используем нативный баннер установки"
);
return;
}
// Проверяем, поддерживает ли браузер установку PWA
if (!this.deferredPrompt) {
console.log("Установка PWA не поддерживается в этом браузере");
return;
}
const installButton = this.createInstallButton();
this.addInstallButtonToPage(installButton);
}
// Проверка на мобильное устройство
isMobileDevice() {
const ua = navigator.userAgent;
@ -142,256 +85,6 @@ class PWAManager {
);
}
// Проверка на мобильный Safari
isMobileSafari() {
const ua = navigator.userAgent;
return (
/iPad|iPhone|iPod/.test(ua) &&
/Safari/.test(ua) &&
!/CriOS|FxiOS|OPiOS|mercury/.test(ua)
);
}
// Создание кнопки установки
createInstallButton() {
const installButton = document.createElement("button");
// Разный текст для разных браузеров
if (this.isMobileSafari()) {
installButton.textContent = "📱 Добавить на главный экран";
} else {
installButton.textContent = "📱 Установить приложение";
}
installButton.className = "btnSave";
installButton.style.marginTop = "10px";
installButton.style.width = "100%";
installButton.style.fontSize = "14px";
installButton.id = "pwa-install-button";
installButton.addEventListener("click", () => {
this.installApp();
});
return installButton;
}
// Добавление кнопки на страницу
addInstallButtonToPage(installButton) {
// Удаляем существующую кнопку, если есть
const existingButton = document.getElementById("pwa-install-button");
if (existingButton) {
existingButton.remove();
}
// Ищем подходящее место для кнопки
const authLink = document.querySelector(".auth-link");
const footer = document.querySelector(".footer");
const container = document.querySelector(".container");
if (authLink) {
authLink.appendChild(installButton);
console.log("Кнопка установки добавлена в auth-link");
} else if (footer) {
footer.insertBefore(installButton, footer.firstChild);
console.log("Кнопка установки добавлена в footer");
} else if (container) {
container.appendChild(installButton);
console.log("Кнопка установки добавлена в container");
} else {
document.body.appendChild(installButton);
console.log("Кнопка установки добавлена в body");
}
}
// Установка приложения
installApp() {
console.log("Попытка установки приложения");
if (this.isMobileSafari()) {
// Для iOS Safari показываем инструкции
this.showSafariInstructions();
return;
}
if (this.deferredPrompt) {
// На мобильных устройствах вызываем prompt() для показа нативного баннера
if (this.isMobileDevice()) {
console.log(
"Показываем нативный баннер установки на мобильном устройстве"
);
this.deferredPrompt.prompt();
this.deferredPrompt.userChoice.then((choiceResult) => {
console.log("Результат установки:", choiceResult.outcome);
if (choiceResult.outcome === "accepted") {
console.log("Пользователь установил приложение");
this.showNotification("Приложение успешно установлено!", "success");
} else {
console.log("Пользователь отклонил установку");
this.showNotification("Установка отменена", "warning");
}
this.deferredPrompt = null;
});
} else {
// На десктопе используем обычную логику
this.deferredPrompt.prompt();
this.deferredPrompt.userChoice.then((choiceResult) => {
console.log("Результат установки:", choiceResult.outcome);
if (choiceResult.outcome === "accepted") {
console.log("Пользователь установил приложение");
this.showNotification("Приложение успешно установлено!", "success");
} else {
console.log("Пользователь отклонил установку");
this.showNotification("Установка отменена", "warning");
}
this.deferredPrompt = null;
this.removeInstallButton();
});
}
} else {
console.log("deferredPrompt не доступен");
this.showManualInstallInstructions();
}
}
// Принудительная проверка возможности установки
async checkInstallability() {
if (!this.isMobileDevice()) {
return false;
}
// Проверяем все требования PWA
const requirements = {
hasManifest: document.querySelector('link[rel="manifest"]') !== null,
hasServiceWorker: "serviceWorker" in navigator,
isSecure:
location.protocol === "https:" || location.hostname === "localhost",
hasIcons: document.querySelector('link[rel="icon"]') !== null,
};
const allRequirementsMet = Object.values(requirements).every((req) => req);
if (!allRequirementsMet) {
console.log("Не все требования PWA выполнены:", requirements);
return false;
}
// Проверяем, есть ли deferredPrompt
if (this.deferredPrompt) {
return true;
}
// Для мобильных устройств без deferredPrompt показываем инструкции
if (this.isMobileDevice()) {
this.showMobileInstallInstructions();
return true;
}
return false;
}
// Показать инструкции для Safari
showSafariInstructions() {
const instructions = `
Для установки приложения на iOS:
1. Нажмите кнопку "Поделиться" () внизу экрана
2. Выберите "На экран Домой"
3. Нажмите "Добавить"
`;
alert(instructions);
}
// Показать инструкции для ручной установки
showManualInstallInstructions() {
const instructions = `
Для установки приложения:
1. Откройте меню браузера ( или )
2. Найдите "Установить приложение" или "Добавить на главный экран"
3. Следуйте инструкциям браузера
`;
alert(instructions);
}
// Показать инструкции для мобильных устройств
showMobileInstallInstructions() {
const isAndroid = /Android/i.test(navigator.userAgent);
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
let instructions = "";
if (isAndroid) {
instructions = `
Для установки приложения на Android:
1. Нажмите на меню браузера ()
2. Выберите "Установить приложение" или "Добавить на главный экран"
3. Подтвердите установку
Или нажмите на иконку установки в адресной строке, если она появилась.
`;
} else if (isIOS) {
instructions = `
Для установки приложения на iOS:
1. Нажмите кнопку "Поделиться" () внизу экрана
2. Выберите "На экран Домой"
3. Нажмите "Добавить"
`;
} else {
instructions = `
Для установки приложения:
1. Откройте меню браузера
2. Найдите "Установить приложение" или "Добавить на главный экран"
3. Следуйте инструкциям браузера
`;
}
alert(instructions);
}
// Удаление кнопки установки
removeInstallButton() {
const installButton = document.getElementById("pwa-install-button");
if (installButton) {
installButton.remove();
console.log("Кнопка установки удалена");
}
}
// Обработка успешной установки
setupAppInstalled() {
window.addEventListener("appinstalled", () => {
console.log("PWA установлено успешно");
this.removeInstallButton();
});
}
// Проверка статуса PWA
isPWAInstalled() {
return (
window.matchMedia("(display-mode: standalone)").matches ||
window.navigator.standalone === true ||
document.referrer.includes("android-app://") ||
window.matchMedia("(display-mode: fullscreen)").matches
);
}
// Получение информации о PWA
getPWAInfo() {
return {
isInstalled: this.isPWAInstalled(),
isOnline: navigator.onLine,
hasServiceWorker: "serviceWorker" in navigator,
userAgent: navigator.userAgent,
hasDeferredPrompt: this.deferredPrompt !== null,
isMobileDevice: this.isMobileDevice(),
isMobileSafari: this.isMobileSafari(),
platform: navigator.platform,
language: navigator.language,
displayMode: window.matchMedia("(display-mode: standalone)").matches
? "standalone"
: "browser",
};
}
// Принудительное обновление кэша
async forceUpdateCache() {
console.log("Принудительное обновление кэша...");
@ -566,7 +259,16 @@ window.PWAManager = pwaManager;
// Добавляем глобальные функции для управления кэшем
window.debugPWA = () => {
console.log("PWA Debug Info:", pwaManager.getPWAInfo());
console.log("PWA Debug Info:", {
isOnline: navigator.onLine,
hasServiceWorker: "serviceWorker" in navigator,
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
displayMode: window.matchMedia("(display-mode: standalone)").matches
? "standalone"
: "browser",
});
};
// Принудительное обновление кэша
@ -604,44 +306,3 @@ window.forceUpdate = async () => {
window.location.reload();
}, 1000);
};
// Проверка возможности установки PWA
window.checkInstallability = () => {
return pwaManager.checkInstallability();
};
// Принудительная попытка установки
window.forceInstall = () => {
if (pwaManager.isMobileDevice()) {
// На мобильных устройствах пытаемся вызвать prompt() если есть deferredPrompt
if (pwaManager.deferredPrompt) {
pwaManager.installApp();
} else {
pwaManager.showMobileInstallInstructions();
}
} else {
pwaManager.showManualInstallInstructions();
}
};
// Показать нативный баннер установки (для мобильных устройств)
window.showInstallBanner = () => {
if (pwaManager.deferredPrompt && pwaManager.isMobileDevice()) {
pwaManager.deferredPrompt.prompt();
pwaManager.deferredPrompt.userChoice.then((choiceResult) => {
console.log("Результат установки:", choiceResult.outcome);
if (choiceResult.outcome === "accepted") {
pwaManager.showNotification(
"Приложение успешно установлено!",
"success"
);
} else {
pwaManager.showNotification("Установка отменена", "warning");
}
pwaManager.deferredPrompt = null;
});
} else {
console.log("Нативный баннер установки недоступен");
pwaManager.showMobileInstallInstructions();
}
};

View File

@ -2,15 +2,24 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Регистрация - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" />
<meta
name="description"
content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря"
/>
<meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="msapplication-TileColor" content="#007bff" />
@ -18,28 +27,58 @@
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
<meta name="application-name" content="NoteJS" />
<meta name="format-detection" content="telephone=no" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/icons/icon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/icons/icon-16x16.png"
/>
<link rel="apple-touch-icon" sizes="57x57" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/icons/icon-48x48.png" />
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="/icons/icon-72x72.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/icons/icon-128x128.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/icons/icon-128x128.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<link
rel="apple-touch-icon"
sizes="114x114"
href="/icons/icon-128x128.png"
/>
<link
rel="apple-touch-icon"
sizes="120x120"
href="/icons/icon-128x128.png"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="/icons/icon-144x144.png"
/>
<link
rel="apple-touch-icon"
sizes="152x152"
href="/icons/icon-152x152.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/icons/icon-192x192.png"
/>
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css" />
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
@ -97,7 +136,7 @@
<p>Создатель: <span>Fovway</span></p>
</div>
<script src="/register.js"></script>
<!-- PWA Script -->
<script src="/pwa.js"></script>
</body>

View File

@ -1,3 +1,7 @@
:root {
--accent-color: #007bff;
}
body {
font-family: "Open Sans", sans-serif;
padding: 0;
@ -175,7 +179,7 @@ header {
.search-input:focus {
outline: none;
border-color: #007bff;
border-color: var(--accent-color, #007bff);
background-color: white;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
@ -205,10 +209,10 @@ header {
margin-top: 5px;
padding: 4px 10px;
background-color: #e7f3ff;
border: 1px solid #007bff;
border: 1px solid var(--accent-color, #007bff);
border-radius: 15px;
font-size: 12px;
color: #007bff;
color: var(--accent-color, #007bff);
font-weight: 500;
}
@ -262,7 +266,7 @@ header {
}
.auth-link a {
color: #007bff;
color: var(--accent-color, #007bff);
text-decoration: none;
font-weight: 500;
}
@ -310,7 +314,7 @@ header {
.form-group input:focus {
outline: none;
border-color: #007bff;
border-color: var(--accent-color, #007bff);
}
.error-message {
@ -360,9 +364,9 @@ textarea:focus {
}
.btnSave:hover {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
border-color: #007bff;
border-color: var(--accent-color, #007bff);
}
.date {
@ -412,7 +416,7 @@ textarea:focus {
background-color: #f8f9fa;
border: 1px solid #ddd;
border-radius: 5px;
color: #007bff;
color: var(--accent-color, #007bff);
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
@ -420,9 +424,9 @@ textarea:focus {
}
.show-more-btn:hover {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
border-color: #007bff;
border-color: var(--accent-color, #007bff);
}
/* Убираем стандартные отступы для абзацев */
@ -466,7 +470,7 @@ textarea:focus {
/* Стили для ссылок */
.textNote a {
color: #007bff;
color: var(--accent-color, #007bff);
text-decoration: none;
word-wrap: break-word;
overflow-wrap: break-word;
@ -478,7 +482,7 @@ textarea:focus {
/* Стили для цитат */
.textNote blockquote {
border-left: 4px solid #007bff;
border-left: 4px solid var(--accent-color, #007bff);
padding-left: 16px;
margin: 10px 0;
color: #555;
@ -599,7 +603,7 @@ textarea:focus {
height: 150px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #007bff;
border: 3px solid var(--accent-color, #007bff);
}
.avatar-placeholder {
@ -634,9 +638,9 @@ textarea:focus {
}
.btn-upload:hover {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
border-color: #007bff;
border-color: var(--accent-color, #007bff);
}
.btn-delete {
@ -700,16 +704,16 @@ textarea:focus {
background-color: #f8f9fa;
border-radius: 5px;
font-size: 14px;
color: #007bff;
color: var(--accent-color, #007bff);
text-decoration: none;
transition: all 0.3s ease;
display: inline-block;
}
.back-btn:hover {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
border-color: #007bff;
border-color: var(--accent-color, #007bff);
}
.username-clickable {
@ -718,7 +722,7 @@ textarea:focus {
}
.username-clickable:hover {
color: #007bff;
color: var(--accent-color, #007bff);
text-decoration: underline;
}
@ -765,7 +769,7 @@ textarea:focus {
border: none;
font-size: 18px;
cursor: pointer;
color: #007bff;
color: var(--accent-color, #007bff);
padding: 0 3px;
transition: color 0.3s ease;
}
@ -815,7 +819,7 @@ textarea:focus {
}
.calendar-day.today {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
font-weight: bold;
}
@ -879,9 +883,9 @@ textarea:focus {
.tag {
display: inline-block;
padding: 4px 8px;
background-color: #e7f3ff;
color: #007bff;
border: 1px solid #007bff;
background-color: #ffffff;
color: var(--accent-color, #007bff);
border: 1px solid var(--accent-color, #007bff);
border-radius: 12px;
font-size: 10px;
font-weight: 500;
@ -891,12 +895,12 @@ textarea:focus {
}
.tag:hover {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
}
.tag.active {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
font-weight: bold;
}
@ -911,9 +915,9 @@ textarea:focus {
.textNote .tag-in-note {
display: inline-block;
padding: 2px 6px;
background-color: #e7f3ff;
color: #007bff;
border: 1px solid #007bff;
background-color: #ffffff;
color: var(--accent-color, #007bff);
border: 1px solid var(--accent-color, #007bff);
border-radius: 8px;
font-size: 12px;
font-weight: 500;
@ -926,7 +930,7 @@ textarea:focus {
}
.textNote .tag-in-note:hover {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
@ -1051,7 +1055,7 @@ textarea:focus {
border: none;
font-size: 16px;
cursor: pointer;
color: #007bff;
color: var(--accent-color, #007bff);
padding: 0 3px;
transition: color 0.3s ease;
}
@ -1089,7 +1093,7 @@ textarea:focus {
}
.mobile-sidebar .calendar-day.today {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
font-weight: bold;
}
@ -1133,7 +1137,7 @@ textarea:focus {
.mobile-sidebar .search-input:focus {
outline: none;
border-color: #007bff;
border-color: var(--accent-color, #007bff);
background-color: white;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
@ -1163,8 +1167,8 @@ textarea:focus {
display: inline-block;
padding: 4px 8px;
background-color: #e7f3ff;
color: #007bff;
border: 1px solid #007bff;
color: var(--accent-color, #007bff);
border: 1px solid var(--accent-color, #007bff);
border-radius: 12px;
font-size: 9px;
font-weight: 500;
@ -1175,12 +1179,12 @@ textarea:focus {
}
.mobile-sidebar .tag:hover {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
}
.mobile-sidebar .tag.active {
background-color: #007bff;
background-color: var(--accent-color, #007bff);
color: white;
font-weight: bold;
}
@ -1223,7 +1227,7 @@ textarea:focus {
.container-leftside {
display: none !important;
}
/* На мобильных устройствах меняем направление flex и центрируем */
body {
flex-direction: column;
@ -1232,7 +1236,7 @@ textarea:focus {
justify-content: flex-start;
align-items: center;
}
/* Центральный контейнер занимает всю ширину, но центрируется */
.center {
width: 100%;
@ -1240,7 +1244,7 @@ textarea:focus {
margin: 0 auto;
margin-top: 60px;
}
/* Адаптируем контейнер заметок */
.container {
width: 100%;
@ -1249,7 +1253,7 @@ textarea:focus {
padding: 10px;
box-sizing: border-box;
}
/* Адаптируем заголовок заметок */
.notes-header {
flex-direction: column;
@ -1257,18 +1261,18 @@ textarea:focus {
gap: 10px;
width: 100%;
}
.notes-header-left {
width: 100%;
}
.user-info {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
/* Адаптируем кнопки markdown */
.markdown-buttons {
display: flex;
@ -1277,7 +1281,7 @@ textarea:focus {
width: 100%;
justify-content: flex-start;
}
.markdown-buttons .btnMarkdown {
flex: 0 1 auto;
min-width: auto;
@ -1285,22 +1289,22 @@ textarea:focus {
padding: 8px 12px;
font-size: 14px;
}
/* Адаптируем textarea */
textarea {
min-height: 100px;
}
/* Адаптируем кнопку сохранения */
.save-button-container {
width: 100%;
flex-direction: column;
}
.btnSave {
width: 100%;
}
/* Адаптируем footer */
.footer {
position: relative;
@ -1407,14 +1411,14 @@ textarea:focus {
object-fit: cover;
border-radius: 6px;
margin: 10px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.note-image:hover {
transform: scale(1.02);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.note-image::after {
@ -1423,7 +1427,7 @@ textarea:focus {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.7);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px;
border-radius: 50%;
@ -1489,7 +1493,7 @@ textarea:focus {
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.9);
background-color: rgba(0, 0, 0, 0.9);
cursor: pointer;
}
@ -1522,37 +1526,91 @@ textarea:focus {
.image-preview-list {
justify-content: center;
}
.note-images-container {
justify-content: center;
}
.image-preview-item img {
width: 80px;
height: 80px;
}
/* Улучшения для мобильных устройств */
.markdown-buttons .btnMarkdown {
min-height: 48px; /* Увеличиваем высоту для touch */
padding: 8px 12px;
margin: 2px;
}
.image-preview-item .remove-image-btn {
width: 28px; /* Еще больше для мобильных */
height: 28px;
font-size: 16px;
}
.clear-images-btn {
min-height: 44px; /* Минимальная высота для touch */
padding: 8px 16px;
font-size: 14px;
}
.note-image {
width: 120px;
height: 120px;
}
}
/* Стили для выбора цветового акцента */
.accent-color-picker {
display: flex;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.color-option {
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
border: 3px solid transparent;
transition: all 0.3s ease;
position: relative;
}
.color-option:hover {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.color-option.selected {
border-color: #333;
transform: scale(1.1);
box-shadow: 0 0 0 2px white, 0 0 0 4px #333;
}
.color-option::after {
content: "✓";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-weight: bold;
font-size: 18px;
opacity: 0;
transition: opacity 0.2s ease;
}
.color-option.selected::after {
opacity: 1;
}
#accentColor {
width: 60px;
height: 40px;
border: none;
border-radius: 5px;
cursor: pointer;
}

View File

@ -1,576 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Тест загрузки изображений на мобильных - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="Тестирование загрузки изображений на мобильных устройствах" />
<meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="apple-touch-fullscreen" content="yes" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css" />
<style>
.test-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.test-section {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.upload-area {
border: 2px dashed #007bff;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
background: #f8f9fa;
margin: 20px 0;
cursor: pointer;
transition: all 0.3s ease;
}
.upload-area:hover {
background: #e7f3ff;
border-color: #0056b3;
}
.upload-area.dragover {
background: #d4edda;
border-color: #28a745;
}
.upload-icon {
font-size: 48px;
color: #007bff;
margin-bottom: 10px;
}
.upload-text {
font-size: 16px;
color: #333;
margin-bottom: 10px;
}
.upload-hint {
font-size: 14px;
color: #6c757d;
}
.file-input {
display: none;
}
.preview-container {
margin: 20px 0;
}
.preview-item {
position: relative;
display: inline-block;
margin: 10px;
border: 1px solid #dee2e6;
border-radius: 6px;
overflow: hidden;
background: white;
}
.preview-item img {
width: 150px;
height: 150px;
object-fit: cover;
display: block;
}
.preview-item .remove-btn {
position: absolute;
top: 5px;
right: 5px;
background: rgba(220, 53, 69, 0.8);
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.preview-item .file-info {
padding: 5px;
font-size: 12px;
color: #6c757d;
background: #f8f9fa;
word-break: break-all;
}
.test-button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
width: 100%;
margin: 10px 0;
}
.test-button:hover {
background: #0056b3;
}
.test-button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.status {
padding: 10px;
border-radius: 4px;
margin: 10px 0;
font-weight: bold;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.debug-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
margin: 10px 0;
}
.device-info {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 6px;
padding: 15px;
margin: 15px 0;
font-size: 14px;
}
</style>
</head>
<body>
<div class="test-container">
<h1>📱 Тест загрузки изображений на мобильных</h1>
<div class="device-info" id="device-info">
Определение устройства...
</div>
<div class="test-section">
<h3>Тест загрузки файлов</h3>
<div class="upload-area" id="upload-area">
<div class="upload-icon">📷</div>
<div class="upload-text">Нажмите для выбора изображений</div>
<div class="upload-hint">или перетащите файлы сюда</div>
<input type="file" id="file-input" class="file-input" accept="image/*" multiple>
</div>
<div class="preview-container" id="preview-container"></div>
<button class="test-button" id="upload-btn" disabled>Загрузить на сервер</button>
<button class="test-button" id="clear-btn">Очистить все</button>
</div>
<div class="test-section">
<h3>Результаты тестов</h3>
<div id="test-results"></div>
</div>
<div class="test-section">
<h3>Отладочная информация</h3>
<button class="test-button" onclick="showDebugInfo()">Показать отладочную информацию</button>
<div id="debug-info" class="debug-info" style="display: none;"></div>
</div>
</div>
<script>
let selectedFiles = [];
// Определение типа устройства
function detectDevice() {
const userAgent = navigator.userAgent;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia('(max-width: 768px)').matches;
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
const isAndroid = /Android/.test(userAgent);
const deviceInfo = document.getElementById('device-info');
let deviceText = '';
if (isMobile) {
deviceText = '📱 <strong>Мобильное устройство</strong><br>';
if (isIOS) {
deviceText += '🍎 iOS устройство<br>';
} else if (isAndroid) {
deviceText += '🤖 Android устройство<br>';
} else {
deviceText += '📱 Другое мобильное устройство<br>';
}
} else {
deviceText = '💻 <strong>ПК/Десктоп</strong><br>';
}
deviceText += `User Agent: ${userAgent}<br>`;
deviceText += `Touch Points: ${navigator.maxTouchPoints || 0}<br>`;
deviceText += `Screen: ${screen.width}x${screen.height}<br>`;
deviceText += `Viewport: ${window.innerWidth}x${window.innerHeight}`;
deviceInfo.innerHTML = deviceText;
return { isMobile, isIOS, isAndroid };
}
// Инициализация
document.addEventListener('DOMContentLoaded', function() {
const device = detectDevice();
setupFileUpload();
runTests(device);
});
// Настройка загрузки файлов
function setupFileUpload() {
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('file-input');
const uploadBtn = document.getElementById('upload-btn');
const clearBtn = document.getElementById('clear-btn');
// Клик по области загрузки
uploadArea.addEventListener('click', function() {
fileInput.click();
});
// Выбор файлов
fileInput.addEventListener('change', function(e) {
handleFiles(e.target.files);
});
// Drag and drop
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', function(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', function(e) {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
// Кнопка загрузки
uploadBtn.addEventListener('click', function() {
uploadFiles();
});
// Кнопка очистки
clearBtn.addEventListener('click', function() {
clearAll();
});
}
// Обработка выбранных файлов
function handleFiles(files) {
const fileArray = Array.from(files);
const imageFiles = fileArray.filter(file => file.type.startsWith('image/'));
if (imageFiles.length === 0) {
showStatus('error', 'Пожалуйста, выберите только изображения');
return;
}
selectedFiles = [...selectedFiles, ...imageFiles];
updatePreview();
updateUploadButton();
showStatus('success', `Добавлено ${imageFiles.length} изображений. Всего: ${selectedFiles.length}`);
}
// Обновление превью
function updatePreview() {
const container = document.getElementById('preview-container');
container.innerHTML = '';
selectedFiles.forEach((file, index) => {
const reader = new FileReader();
reader.onload = function(e) {
const previewItem = document.createElement('div');
previewItem.className = 'preview-item';
previewItem.innerHTML = `
<img src="${e.target.result}" alt="Preview">
<button class="remove-btn" onclick="removeFile(${index})">×</button>
<div class="file-info">
${file.name}<br>
${(file.size / 1024 / 1024).toFixed(2)} MB<br>
${file.type}
</div>
`;
container.appendChild(previewItem);
};
reader.readAsDataURL(file);
});
}
// Удаление файла
function removeFile(index) {
selectedFiles.splice(index, 1);
updatePreview();
updateUploadButton();
}
// Обновление кнопки загрузки
function updateUploadButton() {
const uploadBtn = document.getElementById('upload-btn');
uploadBtn.disabled = selectedFiles.length === 0;
}
// Очистка всех файлов
function clearAll() {
selectedFiles = [];
updatePreview();
updateUploadButton();
document.getElementById('file-input').value = '';
showStatus('info', 'Все файлы очищены');
}
// Загрузка файлов на сервер
async function uploadFiles() {
if (selectedFiles.length === 0) {
showStatus('error', 'Нет файлов для загрузки');
return;
}
const uploadBtn = document.getElementById('upload-btn');
uploadBtn.disabled = true;
uploadBtn.textContent = 'Загрузка...';
try {
const formData = new FormData();
selectedFiles.forEach(file => {
formData.append('images', file);
});
// Создаем тестовую заметку
const noteResponse = await fetch('/api/notes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: 'Тестовая заметка для проверки загрузки изображений',
date: new Date().toLocaleDateString('ru-RU'),
time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
}),
});
if (!noteResponse.ok) {
throw new Error('Ошибка создания заметки');
}
const noteData = await noteResponse.json();
const noteId = noteData.id;
// Загружаем изображения
const uploadResponse = await fetch(`/api/notes/${noteId}/images`, {
method: 'POST',
body: formData,
});
if (!uploadResponse.ok) {
throw new Error('Ошибка загрузки изображений');
}
const uploadData = await uploadResponse.json();
showStatus('success', `Успешно загружено ${uploadData.images.length} изображений!`);
// Очищаем после успешной загрузки
clearAll();
} catch (error) {
console.error('Ошибка загрузки:', error);
showStatus('error', `Ошибка загрузки: ${error.message}`);
} finally {
uploadBtn.disabled = false;
uploadBtn.textContent = 'Загрузить на сервер';
}
}
// Показ статуса
function showStatus(type, message) {
const results = document.getElementById('test-results');
const statusDiv = document.createElement('div');
statusDiv.className = `status ${type}`;
statusDiv.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
results.appendChild(statusDiv);
results.scrollTop = results.scrollHeight;
}
// Запуск тестов
function runTests(device) {
const tests = [
{
name: 'Поддержка File API',
test: () => typeof File !== 'undefined' && typeof FileReader !== 'undefined'
},
{
name: 'Поддержка FormData',
test: () => typeof FormData !== 'undefined'
},
{
name: 'Поддержка fetch API',
test: () => typeof fetch !== 'undefined'
},
{
name: 'Поддержка input[type="file"]',
test: () => {
const input = document.createElement('input');
input.type = 'file';
return input.type === 'file';
}
},
{
name: 'Поддержка multiple атрибута',
test: () => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
return input.multiple === true;
}
},
{
name: 'Поддержка accept атрибута',
test: () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
return input.accept === 'image/*';
}
},
{
name: 'Поддержка Drag and Drop',
test: () => 'draggable' in document.createElement('div')
},
{
name: 'Поддержка Touch Events',
test: () => 'ontouchstart' in window
}
];
tests.forEach(test => {
try {
const result = test.test();
showStatus(result ? 'success' : 'error', `${test.name}: ${result ? '✅ Поддерживается' : '❌ Не поддерживается'}`);
} catch (error) {
showStatus('error', `${test.name}: ❌ Ошибка - ${error.message}`);
}
});
// Дополнительные тесты для мобильных устройств
if (device.isMobile) {
showStatus('info', '📱 Мобильные тесты:');
// Тест размера экрана
const screenSize = screen.width * screen.height;
showStatus('info', `Размер экрана: ${screen.width}x${screen.height} (${screenSize} пикселей)`);
// Тест viewport
const viewportSize = window.innerWidth * window.innerHeight;
showStatus('info', `Размер viewport: ${window.innerWidth}x${window.innerHeight} (${viewportSize} пикселей)`);
// Тест ориентации
const orientation = screen.orientation ? screen.orientation.type : 'unknown';
showStatus('info', `Ориентация: ${orientation}`);
}
}
// Показать отладочную информацию
function showDebugInfo() {
const debugInfo = document.getElementById('debug-info');
const info = {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
onLine: navigator.onLine,
cookieEnabled: navigator.cookieEnabled,
maxTouchPoints: navigator.maxTouchPoints,
screen: {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight
},
window: {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
outerWidth: window.outerWidth,
outerHeight: window.outerHeight
},
devicePixelRatio: window.devicePixelRatio,
selectedFiles: selectedFiles.map(f => ({
name: f.name,
size: f.size,
type: f.type,
lastModified: f.lastModified
}))
};
debugInfo.textContent = JSON.stringify(info, null, 2);
debugInfo.style.display = debugInfo.style.display === 'none' ? 'block' : 'none';
}
</script>
</body>
</html>

View File

@ -1,287 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Тест PWA - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="Тест PWA для NoteJS" />
<meta name="theme-color" content="#007bff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/icons/icon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/icons/icon-16x16.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/icons/icon-192x192.png"
/>
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
}
.success {
background: #d4edda;
color: #155724;
}
.error {
background: #f8d7da;
color: #721c24;
}
.info {
background: #d1ecf1;
color: #0c5460;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #6c757d;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="container">
<h1>🔧 Тест PWA для NoteJS</h1>
<div id="status-container">
<div class="status info">Проверяем требования PWA...</div>
</div>
<div>
<button onclick="checkPWAStatus()">Проверить статус PWA</button>
<button onclick="installPWA()" id="install-btn" disabled>
Установить приложение
</button>
<button onclick="clearCache()">Очистить кэш</button>
</div>
<div
id="debug-info"
style="
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
font-family: monospace;
font-size: 12px;
"
>
<h3>Отладочная информация:</h3>
<div id="debug-content"></div>
</div>
</div>
<!-- PWA Script -->
<script src="/pwa.js"></script>
<script>
let deferredPrompt;
// Проверка статуса PWA
function checkPWAStatus() {
const statusContainer = document.getElementById("status-container");
const debugContent = document.getElementById("debug-content");
// Очищаем предыдущие статусы
statusContainer.innerHTML = "";
debugContent.innerHTML = "";
// Проверяем требования PWA
const checks = [
{
name: "HTTPS или localhost",
status:
location.protocol === "https:" ||
location.hostname === "localhost",
description: `Протокол: ${location.protocol}, Хост: ${location.hostname}`,
},
{
name: "Service Worker",
status: "serviceWorker" in navigator,
description: "Поддержка Service Worker API",
},
{
name: "Manifest",
status: document.querySelector('link[rel="manifest"]') !== null,
description: "Манифест PWA подключен",
},
{
name: "Иконки",
status: document.querySelector('link[rel="icon"]') !== null,
description: "Иконки приложения подключены",
},
{
name: "Уже установлено",
status:
window.matchMedia("(display-mode: standalone)").matches ||
window.navigator.standalone === true,
description: "Приложение уже установлено как PWA",
},
];
let allPassed = true;
checks.forEach((check) => {
const statusDiv = document.createElement("div");
statusDiv.className = `status ${check.status ? "success" : "error"}`;
statusDiv.innerHTML = `${check.status ? "✅" : "❌"} ${check.name}: ${
check.description
}`;
statusContainer.appendChild(statusDiv);
if (!check.status) allPassed = false;
});
// Общая оценка
const overallDiv = document.createElement("div");
overallDiv.className = `status ${allPassed ? "success" : "error"}`;
overallDiv.innerHTML = `${allPassed ? "✅" : "❌"} Общий статус: ${
allPassed ? "PWA готово к установке" : "Есть проблемы с PWA"
}`;
statusContainer.appendChild(overallDiv);
// Отладочная информация
const debugInfo = {
userAgent: navigator.userAgent,
isOnline: navigator.onLine,
hasServiceWorker: "serviceWorker" in navigator,
isStandalone: window.matchMedia("(display-mode: standalone)").matches,
isIOSStandalone: window.navigator.standalone === true,
hasDeferredPrompt: deferredPrompt !== null,
pwaInfo: window.PWAManager
? window.PWAManager.getPWAInfo()
: "PWA Manager не доступен",
};
debugContent.innerHTML = JSON.stringify(debugInfo, null, 2);
// Активируем кнопку установки если доступно
const installBtn = document.getElementById("install-btn");
if (
deferredPrompt &&
!debugInfo.isStandalone &&
!debugInfo.isIOSStandalone
) {
installBtn.disabled = false;
}
}
// Установка PWA
function installPWA() {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
console.log("Результат установки:", choiceResult.outcome);
if (choiceResult.outcome === "accepted") {
alert("Приложение установлено!");
}
deferredPrompt = null;
document.getElementById("install-btn").disabled = true;
});
} else {
alert(
"Установка недоступна. Возможно, приложение уже установлено или браузер не поддерживает установку PWA."
);
}
}
// Очистка кэша
function clearCache() {
if ("caches" in window) {
caches
.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
console.log("Удаление кэша:", cacheName);
return caches.delete(cacheName);
})
);
})
.then(() => {
alert("Кэш очищен! Перезагрузите страницу.");
});
} else {
alert("Кэш не поддерживается в этом браузере.");
}
}
// Обработка события beforeinstallprompt
window.addEventListener("beforeinstallprompt", (e) => {
console.log("beforeinstallprompt событие получено");
// На мобильных устройствах позволяем браузеру показать нативный баннер
const isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
window.matchMedia("(max-width: 768px)").matches;
if (!isMobile) {
e.preventDefault();
}
deferredPrompt = e;
document.getElementById("install-btn").disabled = false;
});
// Обработка успешной установки
window.addEventListener("appinstalled", () => {
console.log("PWA установлено успешно");
alert("Приложение установлено успешно!");
document.getElementById("install-btn").disabled = true;
});
// Автоматическая проверка при загрузке
window.addEventListener("load", () => {
setTimeout(checkPWAStatus, 1000);
});
</script>
</body>
</html>

182
server.js
View File

@ -133,22 +133,22 @@ app.use(
app.use(express.static(path.join(__dirname, "public")));
// PWA файлы с правильными заголовками
app.get('/manifest.json', (req, res) => {
res.setHeader('Content-Type', 'application/manifest+json');
res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 часа
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
app.get("/manifest.json", (req, res) => {
res.setHeader("Content-Type", "application/manifest+json");
res.setHeader("Cache-Control", "public, max-age=86400"); // 24 часа
res.sendFile(path.join(__dirname, "public", "manifest.json"));
});
app.get('/sw.js', (req, res) => {
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
app.get("/sw.js", (req, res) => {
res.setHeader("Content-Type", "application/javascript");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.sendFile(path.join(__dirname, "public", "sw.js"));
});
app.get('/browserconfig.xml', (req, res) => {
res.setHeader('Content-Type', 'application/xml');
res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 часа
res.sendFile(path.join(__dirname, 'public', 'browserconfig.xml'));
app.get("/browserconfig.xml", (req, res) => {
res.setHeader("Content-Type", "application/xml");
res.setHeader("Cache-Control", "public, max-age=86400"); // 24 часа
res.sendFile(path.join(__dirname, "public", "browserconfig.xml"));
});
// Парсинг тела запроса
@ -161,14 +161,14 @@ app.use(
store: new SQLiteStore({
db: "sessions.db",
table: "sessions",
dir: path.join(__dirname, "database")
dir: path.join(__dirname, "database"),
}),
secret: process.env.SESSION_SECRET || "default-secret",
resave: false,
saveUninitialized: false,
cookie: {
cookie: {
secure: false, // в продакшене установить true с HTTPS
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 дней
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней
},
})
);
@ -232,7 +232,10 @@ function createTables() {
db.run(createNoteImagesTable, (err) => {
if (err) {
console.error("Ошибка создания таблицы изображений заметок:", err.message);
console.error(
"Ошибка создания таблицы изображений заметок:",
err.message
);
} else {
console.log("Таблица изображений заметок готова");
}
@ -244,6 +247,7 @@ function createTables() {
} else {
console.log("Таблица пользователей готова");
createIndexes();
runMigrations();
}
});
}
@ -255,7 +259,7 @@ function createIndexes() {
"CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at)",
"CREATE INDEX IF NOT EXISTS idx_notes_date ON notes(date)",
"CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(note_id)",
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)"
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
];
indexes.forEach((indexSql, i) => {
@ -269,6 +273,34 @@ function createIndexes() {
});
}
// Миграции базы данных
function runMigrations() {
// Проверяем существование колонки accent_color и добавляем её если нужно
db.all("PRAGMA table_info(users)", (err, columns) => {
if (err) {
console.error("Ошибка проверки структуры таблицы users:", err.message);
return;
}
const hasAccentColor = columns.some((col) => col.name === "accent_color");
if (!hasAccentColor) {
db.run(
"ALTER TABLE users ADD COLUMN accent_color TEXT DEFAULT '#007bff'",
(err) => {
if (err) {
console.error(
"Ошибка добавления колонки accent_color:",
err.message
);
} else {
console.log("Колонка accent_color добавлена в таблицу users");
}
}
);
}
});
}
// Middleware для аутентификации
function requireAuth(req, res, next) {
if (req.session.authenticated) {
@ -402,10 +434,10 @@ app.post("/login", async (req, res) => {
// API для проверки статуса аутентификации
app.get("/api/auth/status", (req, res) => {
if (req.session.authenticated && req.session.userId) {
res.json({
authenticated: true,
res.json({
authenticated: true,
userId: req.session.userId,
username: req.session.username
username: req.session.username,
});
} else {
res.status(401).json({ authenticated: false });
@ -418,7 +450,8 @@ app.get("/api/user", requireAuth, (req, res) => {
return res.status(401).json({ error: "Не аутентифицирован" });
}
const sql = "SELECT username, email, avatar FROM users WHERE id = ?";
const sql =
"SELECT username, email, avatar, accent_color FROM users WHERE id = ?";
db.get(sql, [req.session.userId], (err, user) => {
if (err) {
console.error("Ошибка получения данных пользователя:", err.message);
@ -435,7 +468,40 @@ app.get("/api/user", requireAuth, (req, res) => {
// Страница с заметками (требует аутентификации)
app.get("/notes", requireAuth, (req, res) => {
res.sendFile(path.join(__dirname, "public", "notes.html"));
// Получаем цвет пользователя для предотвращения FOUC
const sql = "SELECT accent_color FROM users WHERE id = ?";
db.get(sql, [req.session.userId], (err, user) => {
if (err) {
console.error("Ошибка получения цвета пользователя:", err.message);
return res.sendFile(path.join(__dirname, "public", "notes.html"));
}
const accentColor = user?.accent_color || "#007bff";
// Читаем HTML файл
fs.readFile(
path.join(__dirname, "public", "notes.html"),
"utf8",
(err, html) => {
if (err) {
console.error("Ошибка чтения файла notes.html:", err.message);
return res.sendFile(path.join(__dirname, "public", "notes.html"));
}
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
const inlineCSS = `<style>
:root, html { --accent-color: ${accentColor} !important; }
* { --accent-color: ${accentColor} !important; }
</style>`;
const modifiedHtml = html.replace(
/<head>/i,
`<head>\n ${inlineCSS}`
);
res.send(modifiedHtml);
}
);
});
});
// API для поиска заметок с изображениями (должен быть ПЕРЕД /api/notes/:id)
@ -492,13 +558,13 @@ app.get("/api/notes/search", requireAuth, (req, res) => {
console.error("Ошибка поиска заметок:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
// Парсим JSON строки изображений
const notesWithImages = rows.map(row => ({
const notesWithImages = rows.map((row) => ({
...row,
images: row.images === '[]' ? [] : JSON.parse(row.images)
images: row.images === "[]" ? [] : JSON.parse(row.images),
}));
res.json(notesWithImages);
});
});
@ -534,13 +600,13 @@ app.get("/api/notes", requireAuth, (req, res) => {
console.error("Ошибка получения заметок:", err.message);
return res.status(500).json({ error: "Ошибка сервера" });
}
// Парсим JSON строки изображений
const notesWithImages = rows.map(row => ({
const notesWithImages = rows.map((row) => ({
...row,
images: row.images === '[]' ? [] : JSON.parse(row.images)
images: row.images === "[]" ? [] : JSON.parse(row.images),
}));
res.json(notesWithImages);
});
});
@ -738,12 +804,14 @@ app.post(
completed++;
if (completed === files.length) {
if (uploadedImages.length === 0) {
return res.status(500).json({ error: "Не удалось загрузить изображения" });
return res
.status(500)
.json({ error: "Не удалось загрузить изображения" });
}
res.json({
success: true,
res.json({
success: true,
message: `Загружено ${uploadedImages.length} изображений`,
images: uploadedImages
images: uploadedImages,
});
}
});
@ -812,7 +880,8 @@ app.delete("/api/notes/:noteId/images/:imageId", requireAuth, (req, res) => {
}
// Получаем информацию об изображении
const getImageSql = "SELECT file_path FROM note_images WHERE id = ? AND note_id = ?";
const getImageSql =
"SELECT file_path FROM note_images WHERE id = ? AND note_id = ?";
db.get(getImageSql, [imageId, noteId], (err, image) => {
if (err) {
console.error("Ошибка получения изображения:", err.message);
@ -849,12 +918,46 @@ app.delete("/api/notes/:noteId/images/:imageId", requireAuth, (req, res) => {
// Страница личного кабинета
app.get("/profile", requireAuth, (req, res) => {
res.sendFile(path.join(__dirname, "public", "profile.html"));
// Получаем цвет пользователя для предотвращения FOUC
const sql = "SELECT accent_color FROM users WHERE id = ?";
db.get(sql, [req.session.userId], (err, user) => {
if (err) {
console.error("Ошибка получения цвета пользователя:", err.message);
return res.sendFile(path.join(__dirname, "public", "profile.html"));
}
const accentColor = user?.accent_color || "#007bff";
// Читаем HTML файл
fs.readFile(
path.join(__dirname, "public", "profile.html"),
"utf8",
(err, html) => {
if (err) {
console.error("Ошибка чтения файла profile.html:", err.message);
return res.sendFile(path.join(__dirname, "public", "profile.html"));
}
// Вставляем inline CSS с правильным цветом в самое начало head для предотвращения FOUC
const inlineCSS = `<style>
:root, html { --accent-color: ${accentColor} !important; }
* { --accent-color: ${accentColor} !important; }
</style>`;
const modifiedHtml = html.replace(
/<head>/i,
`<head>\n ${inlineCSS}`
);
res.send(modifiedHtml);
}
);
});
});
// API для обновления профиля
app.put("/api/user/profile", requireAuth, async (req, res) => {
const { username, email, currentPassword, newPassword } = req.body;
const { username, email, currentPassword, newPassword, accent_color } =
req.body;
const userId = req.session.userId;
try {
@ -914,6 +1017,11 @@ app.put("/api/user/profile", requireAuth, async (req, res) => {
params.push(email || null);
}
if (accent_color !== undefined) {
updateFields.push("accent_color = ?");
params.push(accent_color || "#007bff");
}
if (newPassword) {
const hashedPassword = await bcrypt.hash(newPassword, 10);
updateFields.push("password = ?");

View File

@ -1,292 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Тест загрузки изображений</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.test-section {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.success {
color: green;
}
.error {
color: red;
}
.info {
color: blue;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
input[type="file"] {
margin: 10px 0;
}
.image-preview {
max-width: 200px;
max-height: 200px;
margin: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>Тест функциональности загрузки изображений</h1>
<div class="test-section">
<h2>1. Регистрация тестового пользователя</h2>
<button onclick="registerTestUser()">Зарегистрировать тестового пользователя</button>
<div id="registerResult"></div>
</div>
<div class="test-section">
<h2>2. Вход в систему</h2>
<button onclick="loginTestUser()">Войти как тестовый пользователь</button>
<div id="loginResult"></div>
</div>
<div class="test-section">
<h2>3. Создание заметки с изображениями</h2>
<input type="file" id="imageInput" accept="image/*" multiple>
<button onclick="createNoteWithImages()">Создать заметку с изображениями</button>
<div id="noteResult"></div>
<div id="imagePreviews"></div>
</div>
<div class="test-section">
<h2>4. Получение изображений заметки</h2>
<input type="number" id="noteIdInput" placeholder="ID заметки">
<button onclick="getNoteImages()">Получить изображения</button>
<div id="getImagesResult"></div>
</div>
<div class="test-section">
<h2>5. Удаление изображения</h2>
<input type="number" id="deleteNoteIdInput" placeholder="ID заметки">
<input type="number" id="deleteImageIdInput" placeholder="ID изображения">
<button onclick="deleteImage()">Удалить изображение</button>
<div id="deleteResult"></div>
</div>
<script>
let authToken = null;
let selectedFiles = [];
// Обработчик выбора файлов
document.getElementById('imageInput').addEventListener('change', function(e) {
selectedFiles = Array.from(e.target.files);
displayImagePreviews();
});
function displayImagePreviews() {
const container = document.getElementById('imagePreviews');
container.innerHTML = '';
selectedFiles.forEach((file, index) => {
const reader = new FileReader();
reader.onload = function(e) {
const img = document.createElement('img');
img.src = e.target.result;
img.className = 'image-preview';
img.title = file.name;
container.appendChild(img);
};
reader.readAsDataURL(file);
});
}
async function registerTestUser() {
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: 'testuser',
password: 'testpass123',
confirmPassword: 'testpass123'
})
});
const result = await response.json();
const resultDiv = document.getElementById('registerResult');
if (response.ok) {
resultDiv.innerHTML = '<div class="success">✓ Пользователь успешно зарегистрирован</div>';
} else {
resultDiv.innerHTML = `<div class="error">✗ Ошибка: ${result.error}</div>`;
}
} catch (error) {
document.getElementById('registerResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
}
}
async function loginTestUser() {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: 'testuser',
password: 'testpass123'
})
});
const result = await response.json();
const resultDiv = document.getElementById('loginResult');
if (response.ok) {
resultDiv.innerHTML = '<div class="success">✓ Успешный вход в систему</div>';
authToken = 'authenticated'; // В реальном приложении здесь был бы JWT токен
} else {
resultDiv.innerHTML = `<div class="error">✗ Ошибка: ${result.error}</div>`;
}
} catch (error) {
document.getElementById('loginResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
}
}
async function createNoteWithImages() {
if (!authToken) {
document.getElementById('noteResult').innerHTML = '<div class="error">✗ Сначала войдите в систему</div>';
return;
}
try {
// Сначала создаем заметку
const noteResponse = await fetch('/api/notes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: 'Тестовая заметка с изображениями',
date: new Date().toLocaleDateString('ru-RU'),
time: new Date().toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'})
})
});
const noteResult = await noteResponse.json();
if (!noteResponse.ok) {
throw new Error(noteResult.error);
}
const noteId = noteResult.id;
document.getElementById('noteResult').innerHTML = `<div class="success">✓ Заметка создана с ID: ${noteId}</div>`;
// Теперь загружаем изображения
if (selectedFiles.length > 0) {
const formData = new FormData();
selectedFiles.forEach(file => {
formData.append('images', file);
});
const imageResponse = await fetch(`/api/notes/${noteId}/images`, {
method: 'POST',
body: formData
});
const imageResult = await imageResponse.json();
if (imageResponse.ok) {
document.getElementById('noteResult').innerHTML += `<div class="success">✓ Загружено ${imageResult.images.length} изображений</div>`;
document.getElementById('noteIdInput').value = noteId;
} else {
document.getElementById('noteResult').innerHTML += `<div class="error">✗ Ошибка загрузки изображений: ${imageResult.error}</div>`;
}
} else {
document.getElementById('noteResult').innerHTML += '<div class="info"> Изображения не выбраны</div>';
}
} catch (error) {
document.getElementById('noteResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
}
}
async function getNoteImages() {
const noteId = document.getElementById('noteIdInput').value;
if (!noteId) {
document.getElementById('getImagesResult').innerHTML = '<div class="error">✗ Введите ID заметки</div>';
return;
}
try {
const response = await fetch(`/api/notes/${noteId}/images`);
const result = await response.json();
if (response.ok) {
const resultDiv = document.getElementById('getImagesResult');
resultDiv.innerHTML = `<div class="success">✓ Найдено ${result.length} изображений:</div>`;
result.forEach(image => {
resultDiv.innerHTML += `
<div style="margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
<img src="${image.file_path}" style="max-width: 100px; max-height: 100px; margin-right: 10px;">
<div>
<strong>ID:</strong> ${image.id}<br>
<strong>Имя файла:</strong> ${image.original_name}<br>
<strong>Размер:</strong> ${(image.file_size / 1024).toFixed(2)} KB<br>
<strong>Тип:</strong> ${image.mime_type}
</div>
</div>
`;
});
} else {
document.getElementById('getImagesResult').innerHTML = `<div class="error">✗ Ошибка: ${result.error}</div>`;
}
} catch (error) {
document.getElementById('getImagesResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
}
}
async function deleteImage() {
const noteId = document.getElementById('deleteNoteIdInput').value;
const imageId = document.getElementById('deleteImageIdInput').value;
if (!noteId || !imageId) {
document.getElementById('deleteResult').innerHTML = '<div class="error">✗ Введите ID заметки и ID изображения</div>';
return;
}
try {
const response = await fetch(`/api/notes/${noteId}/images/${imageId}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok) {
document.getElementById('deleteResult').innerHTML = '<div class="success">✓ Изображение успешно удалено</div>';
} else {
document.getElementById('deleteResult').innerHTML = `<div class="error">✗ Ошибка: ${result.error}</div>`;
}
} catch (error) {
document.getElementById('deleteResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
}
}
</script>
</body>
</html>