Compare commits
2 Commits
02be77d790
...
b831dcc52c
| Author | SHA1 | Date | |
|---|---|---|---|
| b831dcc52c | |||
| 8354e64ae7 |
@ -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` - специальная тестовая страница
|
||||
- Консоль разработчика в мобильном браузере
|
||||
- Информация об устройстве и браузере на тестовой странице
|
||||
269
PWA-TESTING.md
269
PWA-TESTING.md
@ -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-уведомления (опционально)
|
||||
359
public/app.js
359
public/app.js
@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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">×</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 () {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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>
|
||||
361
public/pwa.js
361
public/pwa.js
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
176
public/style.css
176
public/style.css
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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
182
server.js
@ -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 = ?");
|
||||
|
||||
292
test_images.html
292
test_images.html
@ -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>
|
||||
Loading…
x
Reference in New Issue
Block a user