Compare commits
2 Commits
e08e220c19
...
c53f174959
| Author | SHA1 | Date | |
|---|---|---|---|
| c53f174959 | |||
| 5c11d21290 |
101
lzipus/CHANGELOG.md
Normal file
101
lzipus/CHANGELOG.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Все значимые изменения в проекте Izipus документируются в этом файле.
|
||||||
|
|
||||||
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
|
||||||
|
версионирование следует [Semantic Versioning](https://semver.org/lang/ru/).
|
||||||
|
|
||||||
|
## [0.4.0] - 2025-10-26
|
||||||
|
|
||||||
|
### Добавлено
|
||||||
|
- 🎉 **Toast уведомления**: Визуальное подтверждение при копировании шаблонов с отображением счётчика использований
|
||||||
|
- 🔍 **Поиск по шаблонам**: Мгновенный поиск по названию и содержимому шаблонов (RU/EN)
|
||||||
|
- Автоматическое открытие категорий с найденными шаблонами
|
||||||
|
- Скрытие категорий без результатов
|
||||||
|
- 📤 **Экспорт шаблонов**: Сохранение пользовательских шаблонов в JSON файл
|
||||||
|
- Формат имени файла: `izipus-templates-YYYY-MM-DD.json`
|
||||||
|
- Валидация данных перед экспортом
|
||||||
|
- 📥 **Импорт шаблонов**: Загрузка шаблонов из JSON файла
|
||||||
|
- Автоматическая валидация структуры
|
||||||
|
- Предотвращение дубликатов по названию
|
||||||
|
- Информирование о количестве импортированных шаблонов
|
||||||
|
- 📊 **Счётчик использования**: Отслеживание частоты использования каждого шаблона
|
||||||
|
- Отображение в toast уведомлениях
|
||||||
|
- Раздельная статистика для встроенных и пользовательских шаблонов
|
||||||
|
- 🖊️ **Редактирование встроенных шаблонов**: Возможность переопределять встроенные шаблоны
|
||||||
|
- Система overrides с сохранением в chrome.storage
|
||||||
|
- Выбор языка при редактировании (RU/EN)
|
||||||
|
- 💾 **Chrome Storage Sync API**: Синхронизация данных между устройствами
|
||||||
|
- Автоматическая миграция из localStorage
|
||||||
|
- Fallback на localStorage для локального тестирования
|
||||||
|
- Синхронизация: шаблонов, статистики, темы, состояния категорий
|
||||||
|
- 🎨 **Улучшенный UI заголовка**: Группировка кнопок управления
|
||||||
|
- Кнопки добавления, импорта и экспорта объединены в header-buttons
|
||||||
|
|
||||||
|
### Изменено
|
||||||
|
- 📦 **Версия**: Обновлена с 0.3.1 до 0.4.0
|
||||||
|
- 🔄 **Storage API**: Все операции с хранилищем переведены на chrome.storage.sync
|
||||||
|
- `templates` - пользовательские шаблоны
|
||||||
|
- `builtinOverrides` - переопределения встроенных шаблонов
|
||||||
|
- `usageStats` - статистика использования
|
||||||
|
- `theme` - текущая тема
|
||||||
|
- `categoryStates` - состояния категорий
|
||||||
|
- ⚡ **Асинхронные операции**: Переход на async/await для всех операций хранения
|
||||||
|
- 🎯 **Header layout**: Изменён с `justify-content: center` на `space-between`
|
||||||
|
|
||||||
|
### Исправлено
|
||||||
|
- 🐛 **HTML опечатка**: Исправлен тег `</1--div>` на корректный `</div>` (строка 280 в popup.html)
|
||||||
|
- ✨ **Анимации**: Улучшены анимации toast уведомлений
|
||||||
|
- slideIn: плавное появление справа
|
||||||
|
- fadeOut: постепенное исчезновение через 2.7 секунды
|
||||||
|
|
||||||
|
### Технические детали
|
||||||
|
- Добавлен `StorageHelper` - универсальная обёртка для работы с хранилищем
|
||||||
|
- Функция `migrateFromLocalStorage()` для одноразовой миграции данных
|
||||||
|
- Новые CSS классы: `.toast-container`, `.toast`, `.search-wrapper`, `.hidden-by-search`, `.header-buttons`
|
||||||
|
- Улучшена структура кода с использованием async/await
|
||||||
|
|
||||||
|
### Производительность
|
||||||
|
- Оптимизирован поиск с использованием `includes()` вместо регулярных выражений
|
||||||
|
- Кэширование состояния категорий для быстрого доступа
|
||||||
|
- Ленивая загрузка шаблонов при импорте
|
||||||
|
|
||||||
|
## [0.3.1] - Предыдущая версия
|
||||||
|
|
||||||
|
### Возможности
|
||||||
|
- Базовая библиотека встроенных шаблонов (60+ шаблонов)
|
||||||
|
- Создание пользовательских шаблонов
|
||||||
|
- Организация по категориям:
|
||||||
|
- Промежуточные ответы
|
||||||
|
- Запросы партнерам
|
||||||
|
- Ответы клиентам
|
||||||
|
- Даофис (специфичные шаблоны)
|
||||||
|
- Тёмная/светлая тема
|
||||||
|
- Сворачивание категорий с сохранением состояния
|
||||||
|
- Копирование в буфер обмена одним кликом
|
||||||
|
- Редактирование и удаление пользовательских шаблонов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Планы на будущее
|
||||||
|
|
||||||
|
### [0.5.0] - Фаза 2: Переменные и история
|
||||||
|
- Система переменных: `{{client_name}}`, `{{ticket_id}}`, `{{date}}`
|
||||||
|
- История последних использованных шаблонов
|
||||||
|
- Категория "Недавние" для быстрого доступа
|
||||||
|
- Графики и статистика популярности шаблонов
|
||||||
|
|
||||||
|
### [0.6.0] - Фаза 2: Интеграция
|
||||||
|
- Context menu для быстрого доступа
|
||||||
|
- Content script для вставки в текстовые поля
|
||||||
|
- Горячие клавиши для избранных шаблонов
|
||||||
|
|
||||||
|
### [1.0.0] - Стабильный релиз
|
||||||
|
- Полная локализация интерфейса
|
||||||
|
- Интеграция с Zendesk/Jira
|
||||||
|
- Облачная синхронизация для команд
|
||||||
|
- Полное тестовое покрытие
|
||||||
|
|
||||||
|
[0.4.0]: https://git.gorshenin.info/Dgors03/Answer_Templates/compare/v0.3.1...v0.4.0
|
||||||
|
[0.3.1]: https://git.gorshenin.info/Dgors03/Answer_Templates/releases/tag/v0.3.1
|
||||||
|
|
||||||
166
lzipus/README.md
Normal file
166
lzipus/README.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# Izipus - SMS Support Templates Manager
|
||||||
|
|
||||||
|
**Version:** 0.4.0
|
||||||
|
**Type:** Browser Extension (Firefox/Chrome)
|
||||||
|
|
||||||
|
## 🎯 Описание
|
||||||
|
|
||||||
|
Izipus - браузерное расширение для специалистов технической поддержки SMS/телеком-операторов. Упрощает работу с шаблонами ответов на типовые запросы клиентов и партнёров.
|
||||||
|
|
||||||
|
## ✨ Основные возможности
|
||||||
|
|
||||||
|
### Фаза 1 (v0.4.0) - ГОТОВО ✅
|
||||||
|
|
||||||
|
#### 📝 Управление шаблонами
|
||||||
|
- **Библиотека готовых шаблонов** на русском и английском языках
|
||||||
|
- **Создание пользовательских шаблонов** с RU/EN версиями
|
||||||
|
- **Редактирование встроенных шаблонов** с сохранением переопределений
|
||||||
|
- **Импорт/экспорт шаблонов** в JSON для обмена с коллегами
|
||||||
|
- **Поиск по шаблонам** по названию и содержимому
|
||||||
|
|
||||||
|
#### 💾 Синхронизация
|
||||||
|
- **Chrome Storage Sync API** - автоматическая синхронизация между устройствами
|
||||||
|
- **Автоматическая миграция** данных из localStorage
|
||||||
|
- **Fallback на localStorage** для локального тестирования
|
||||||
|
|
||||||
|
#### 🎨 Пользовательский интерфейс
|
||||||
|
- **Toast уведомления** при копировании в буфер обмена
|
||||||
|
- **Тёмная/светлая тема** с сохранением выбора
|
||||||
|
- **Организация по категориям** с возможностью сворачивания
|
||||||
|
- **Счётчик использования** для каждого шаблона
|
||||||
|
- **Адаптивный дизайн**
|
||||||
|
|
||||||
|
## 🚀 Установка
|
||||||
|
|
||||||
|
### Firefox
|
||||||
|
1. Скачайте расширение
|
||||||
|
2. Откройте `about:debugging#/runtime/this-firefox`
|
||||||
|
3. Нажмите "Загрузить временное дополнение"
|
||||||
|
4. Выберите файл `manifest.json`
|
||||||
|
|
||||||
|
### Chrome/Edge
|
||||||
|
1. Скачайте расширение
|
||||||
|
2. Откройте `chrome://extensions/`
|
||||||
|
3. Включите "Режим разработчика"
|
||||||
|
4. Нажмите "Загрузить распакованное расширение"
|
||||||
|
5. Выберите папку с расширением
|
||||||
|
|
||||||
|
## 📖 Использование
|
||||||
|
|
||||||
|
### Основные действия
|
||||||
|
|
||||||
|
1. **Копирование шаблона**: Нажмите кнопку RU или ENG рядом с нужным шаблоном
|
||||||
|
2. **Создание шаблона**: Нажмите кнопку "+" в заголовке
|
||||||
|
3. **Редактирование**: Нажмите иконку карандаша рядом с шаблоном
|
||||||
|
4. **Удаление**: Нажмите иконку корзины
|
||||||
|
5. **Поиск**: Используйте строку поиска в верхней части окна
|
||||||
|
|
||||||
|
### Импорт/Экспорт
|
||||||
|
|
||||||
|
**Экспорт:**
|
||||||
|
- Нажмите кнопку экспорта (📤)
|
||||||
|
- Файл сохранится как `izipus-templates-YYYY-MM-DD.json`
|
||||||
|
|
||||||
|
**Импорт:**
|
||||||
|
- Нажмите кнопку импорта (📥)
|
||||||
|
- Выберите JSON файл с шаблонами
|
||||||
|
- Дубликаты (по названию) будут автоматически пропущены
|
||||||
|
|
||||||
|
### Редактирование встроенных шаблонов
|
||||||
|
|
||||||
|
1. Нажмите кнопку редактирования (🖊️) на встроенном шаблоне
|
||||||
|
2. Выберите язык (OK - RU, Отмена - EN)
|
||||||
|
3. Введите новый текст
|
||||||
|
4. Изменения сохраняются автоматически
|
||||||
|
|
||||||
|
## 📂 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
lzipus/
|
||||||
|
├── manifest.json # Конфигурация расширения
|
||||||
|
├── icons/ # Иконки расширения
|
||||||
|
│ ├── icon16.png
|
||||||
|
│ ├── icon48.png
|
||||||
|
│ └── icon128.png
|
||||||
|
└── src/
|
||||||
|
├── popup.html # Интерфейс расширения
|
||||||
|
├── popup.css # Стили
|
||||||
|
└── popup.js # Логика приложения
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Технологии
|
||||||
|
|
||||||
|
- **Manifest V3** - современная версия API расширений
|
||||||
|
- **Chrome Storage Sync API** - синхронизация данных
|
||||||
|
- **Bootstrap 5.3.1** - UI компоненты
|
||||||
|
- **Font Awesome 6.1.2** - иконки
|
||||||
|
- **Vanilla JavaScript** - без фреймворков
|
||||||
|
|
||||||
|
## 📊 Хранение данных
|
||||||
|
|
||||||
|
Все данные хранятся в `chrome.storage.sync`:
|
||||||
|
|
||||||
|
- `templates` - пользовательские шаблоны
|
||||||
|
- `builtinOverrides` - переопределения встроенных шаблонов
|
||||||
|
- `usageStats` - статистика использования
|
||||||
|
- `theme` - выбранная тема
|
||||||
|
- `categoryStates` - состояния категорий (открыто/закрыто)
|
||||||
|
|
||||||
|
## 🗺️ Дорожная карта
|
||||||
|
|
||||||
|
### Фаза 2: Новая функциональность
|
||||||
|
- [ ] Система переменных в шаблонах (`{{client_name}}`, `{{date}}`)
|
||||||
|
- [ ] Контекстное меню для быстрого доступа
|
||||||
|
- [ ] История использования и статистика
|
||||||
|
- [ ] Мультиязычность (ES, DE, FR)
|
||||||
|
|
||||||
|
### Фаза 3: Интеграция
|
||||||
|
- [ ] Интеграция с CRM/тикет-системами (Zendesk, Jira)
|
||||||
|
- [ ] AI-ассистент для предложения шаблонов
|
||||||
|
- [ ] Командные возможности (облачное хранилище)
|
||||||
|
|
||||||
|
### Фаза 4: Качество
|
||||||
|
- [ ] Рефакторинг (модули, TypeScript)
|
||||||
|
- [ ] Unit и E2E тесты
|
||||||
|
- [ ] CI/CD pipeline
|
||||||
|
- [ ] Полная документация
|
||||||
|
|
||||||
|
## 🤝 Вклад в проект
|
||||||
|
|
||||||
|
Проект открыт для улучшений! Если у вас есть идеи или вы нашли баг:
|
||||||
|
|
||||||
|
1. Создайте issue на GitLab
|
||||||
|
2. Предложите pull request
|
||||||
|
3. Опишите изменения и их необходимость
|
||||||
|
|
||||||
|
## 📝 Changelog
|
||||||
|
|
||||||
|
### v0.4.0 (2025-10-26) - Фаза 1
|
||||||
|
- ✅ Toast уведомления при копировании
|
||||||
|
- ✅ Поиск по шаблонам
|
||||||
|
- ✅ Импорт/экспорт в JSON
|
||||||
|
- ✅ Счётчик использования шаблонов
|
||||||
|
- ✅ Миграция на chrome.storage.sync
|
||||||
|
- ✅ Редактирование встроенных шаблонов
|
||||||
|
- 🐛 Исправлена опечатка в HTML
|
||||||
|
|
||||||
|
### v0.3.1
|
||||||
|
- Базовая функциональность
|
||||||
|
- Встроенные шаблоны
|
||||||
|
- Пользовательские шаблоны
|
||||||
|
- Тёмная тема
|
||||||
|
|
||||||
|
## 📄 Лицензия
|
||||||
|
|
||||||
|
Проект распространяется "как есть" для внутреннего использования.
|
||||||
|
|
||||||
|
## 👤 Автор
|
||||||
|
|
||||||
|
**Dmitriy Gorshenin**
|
||||||
|
Email: dmitriy.gorshenin1@gmail.com
|
||||||
|
GitLab: https://git.gorshenin.info/Dgors03/Answer_Templates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Made with ❤️ for SMS Support Teams
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Izipus",
|
"name": "Izipus",
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"description": "Lzipus - assists your workflow by simplifying interactions.",
|
"description": "Lzipus - assists your workflow by simplifying interactions.",
|
||||||
"homepage_url": "https://git.gorshenin.info/Dgors03/Answer_Templates",
|
"homepage_url": "https://git.gorshenin.info/Dgors03/Answer_Templates",
|
||||||
|
|||||||
@@ -8,6 +8,28 @@ body {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Плавное появление элементов */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Заголовки */
|
||||||
|
h1, h3 {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 16px; /* Уменьшен размер шрифта */
|
||||||
|
font-weight: normal; /* Уменьшен вес шрифта для минималистичного вида */
|
||||||
|
color: #333; /* Цвет шрифта для категорий */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Контейнеры */
|
||||||
div.container {
|
div.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -18,26 +40,17 @@ div.container {
|
|||||||
div.header {
|
div.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.header-buttons {
|
||||||
padding: 6px;
|
display: flex;
|
||||||
}
|
gap: 5px;
|
||||||
|
|
||||||
h3 {
|
|
||||||
padding: 6px;
|
|
||||||
font-family: Helvetica, Arial, sans-serif; /* Более минималистичный шрифт для категорий */
|
|
||||||
font-size: 16px; /* Уменьшен размер шрифта */
|
|
||||||
font-weight: normal; /* Уменьшен вес шрифта для минималистичного вида */
|
|
||||||
color: #333; /* Цвет шрифта для категорий */
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
width: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Кнопки */
|
||||||
button.template-add,
|
button.template-add,
|
||||||
button.template-edit,
|
button.template-edit,
|
||||||
button.template-delete {
|
button.template-delete {
|
||||||
@@ -49,19 +62,19 @@ button.template-delete {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px; /* Меньший размер текста */
|
font-size: 14px; /* Меньший размер текста */
|
||||||
border-radius: 5px; /* Скругленные углы */
|
border-radius: 5px; /* Скругленные углы */
|
||||||
transition: background-color 0.3s, color 0.3s, border-color 0.3s; /* Плавные переходы */
|
transition: background-color 0.3s, color 0.3s, border-color 0.3s, transform 0.3s; /* Плавные переходы */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Эффект при наведении */
|
/* Эффекты для кнопок */
|
||||||
button.template-add:hover,
|
button.template-add:hover,
|
||||||
button.template-edit:hover,
|
button.template-edit:hover,
|
||||||
button.template-delete:hover {
|
button.template-delete:hover {
|
||||||
background-color: #abcef5; /* Подсветка фоном */
|
background-color: #abcef5; /* Подсветка фоном */
|
||||||
color: #fff; /* Белый цвет текста */
|
color: #fff; /* Белый цвет текста */
|
||||||
border-color: #88aee5; /* Немного темнее рамка */
|
border-color: #88aee5; /* Немного темнее рамка */
|
||||||
|
transform: scale(1.05); /* Увеличение кнопки при наведении */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Эффект при фокусе */
|
|
||||||
button.template-add:focus,
|
button.template-add:focus,
|
||||||
button.template-edit:focus,
|
button.template-edit:focus,
|
||||||
button.template-delete:focus {
|
button.template-delete:focus {
|
||||||
@@ -69,6 +82,7 @@ button.template-delete:focus {
|
|||||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); /* Легкая тень для фокуса */
|
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); /* Легкая тень для фокуса */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Шаблоны */
|
||||||
div.templates {
|
div.templates {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -83,12 +97,16 @@ span.template {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
padding: 8px 7px;
|
padding: 8px 7px;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: fit-content;
|
||||||
|
padding: 8px 7px;
|
||||||
border: 1px solid #8888c6; /* Цвет рамки совпадает с рамкой категории */
|
border: 1px solid #8888c6; /* Цвет рамки совпадает с рамкой категории */
|
||||||
border-radius: 8px; /* Скругленные углы */
|
border-radius: 8px; /* Скругленные углы */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%; /* Шаблоны должны растягиваться на всю ширину */
|
width: 100%; /* Шаблоны должны растягиваться на всю ширину */
|
||||||
box-sizing: border-box; /* Чтобы паддинги не влияли на размер */
|
box-sizing: border-box; /* Чтобы паддинги не влияли на размер */
|
||||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; /* Плавный переход при наведении */
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; /* Плавный переход при наведении */
|
||||||
|
animation: fadeIn 0.5s ease-in; /* Применяем анимацию к элементам шаблона */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Подсветка шаблонов при наведении */
|
/* Подсветка шаблонов при наведении */
|
||||||
@@ -265,3 +283,112 @@ textarea {
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toast уведомления */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 250px;
|
||||||
|
animation: slideIn 0.3s ease-out, fadeOut 0.3s ease-in 2.7s;
|
||||||
|
opacity: 0;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .toast {
|
||||||
|
background: #388E3C;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Поиск */
|
||||||
|
.search-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin: 10px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-wrapper input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 40px 10px 15px;
|
||||||
|
border: 1px solid #8888c6;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-wrapper input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #a56fbf;
|
||||||
|
box-shadow: 0 0 5px rgba(165, 111, 191, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #8888c6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .search-wrapper input {
|
||||||
|
background: #444;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .search-icon {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скрытие элементов при поиске */
|
||||||
|
.hidden-by-search {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,16 +11,34 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div class="toast-container" id="toast-container"></div>
|
||||||
<button id="theme-toggle" class="theme-btn">
|
<button id="theme-toggle" class="theme-btn">
|
||||||
<i id="theme-icon" class="fa-moon fa-solid"></i>
|
<i id="theme-icon" class="fa-moon fa-solid"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1 class="name fs-4 fw-lighter font-monospace">Твои шаблоны</h1>
|
<h1 class="name fs-4 fw-lighter font-monospace">Твои шаблоны</h1>
|
||||||
<button class="template-add btn btn-primary" type="button"><i class="fa-duotone fa-plus"></i></button>
|
<div class="header-buttons">
|
||||||
|
<button class="template-add btn btn-primary" type="button" title="Добавить шаблон">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
<button class="import-btn btn btn-secondary" type="button" title="Импортировать шаблоны">
|
||||||
|
<i class="fa-solid fa-file-import"></i>
|
||||||
|
</button>
|
||||||
|
<button class="export-btn btn btn-secondary" type="button" title="Экспортировать шаблоны">
|
||||||
|
<i class="fa-solid fa-file-export"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="templates" id="template-list">
|
<input type="file" id="import-file-input" accept=".json" style="display: none;">
|
||||||
|
|
||||||
|
<div class="search-wrapper">
|
||||||
|
<input type="text" id="search-input" class="form-control" placeholder="Поиск по шаблонам...">
|
||||||
|
<i class="fa-solid fa-search search-icon"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="templates" id="template-list">
|
||||||
<div class="category">
|
<div class="category">
|
||||||
<h3 class="font-monospace">
|
<h3 class="font-monospace">
|
||||||
Промежуточные ответы
|
Промежуточные ответы
|
||||||
@@ -28,12 +46,12 @@
|
|||||||
|
|
||||||
</h3>
|
</h3>
|
||||||
<div class="category-content">
|
<div class="category-content">
|
||||||
<span class="template">
|
<span class="template" data-builtin-id="inWork">
|
||||||
<span class="template-title font-monospace">Приняли в работу</span>
|
<span class="template-title font-monospace">Приняли в работу</span>
|
||||||
<span class="buttons">
|
<span class="buttons">
|
||||||
<span class="btn btn-light" id="inWorkRu">RU</span>
|
<span class="btn btn-light" id="inWorkRu">RU</span>
|
||||||
<span class="btn btn-light" id="inWorkEn">ENG</span>
|
<span class="btn btn-light" id="inWorkEn">ENG</span>
|
||||||
<!-- <button class="template-edit-existed btn"><i class="fa-solid fa-pen-to-square"></i></button> -->
|
<button class="template-edit-builtin btn"><i class="fa-solid fa-pen-to-square"></i></button>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="template">
|
<span class="template">
|
||||||
@@ -277,7 +295,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</1--div>
|
</div>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,124 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
// Storage Helper - обёртка для chrome.storage.sync с fallback на localStorage
|
||||||
|
const StorageHelper = {
|
||||||
|
async get(key) {
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.sync.get([key], (result) => {
|
||||||
|
resolve(result[key]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback для локального тестирования
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
return value ? JSON.parse(value) : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async set(key, value) {
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.sync.set({ [key]: value }, resolve);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback для локального тестирования
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove(key) {
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.sync.remove(key, resolve);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Миграция данных из localStorage в chrome.storage.sync
|
||||||
|
async function migrateFromLocalStorage() {
|
||||||
|
const migrationKey = 'migrated_to_chrome_storage';
|
||||||
|
const alreadyMigrated = await StorageHelper.get(migrationKey);
|
||||||
|
|
||||||
|
if (!alreadyMigrated && typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
|
||||||
|
console.log('Начинаем миграцию данных из localStorage в chrome.storage.sync...');
|
||||||
|
|
||||||
|
const keysToMigrate = ['templates', 'theme', 'usageStats'];
|
||||||
|
|
||||||
|
for (const key of keysToMigrate) {
|
||||||
|
const localValue = localStorage.getItem(key);
|
||||||
|
if (localValue) {
|
||||||
|
try {
|
||||||
|
const parsedValue = JSON.parse(localValue);
|
||||||
|
await StorageHelper.set(key, parsedValue);
|
||||||
|
console.log(`Мигрирован ключ: ${key}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Ошибка при миграции ключа ${key}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Миграция состояний категорий
|
||||||
|
const categoryStates = {};
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key && !keysToMigrate.includes(key)) {
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
if (value === 'true' || value === 'false') {
|
||||||
|
categoryStates[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(categoryStates).length > 0) {
|
||||||
|
await StorageHelper.set('categoryStates', categoryStates);
|
||||||
|
console.log('Мигрированы состояния категорий');
|
||||||
|
}
|
||||||
|
|
||||||
|
await StorageHelper.set(migrationKey, true);
|
||||||
|
console.log('Миграция завершена!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await migrateFromLocalStorage();
|
||||||
|
|
||||||
|
// Статистика использования шаблонов
|
||||||
|
let usageStats = await StorageHelper.get('usageStats') || {};
|
||||||
|
|
||||||
|
async function updateUsageStats(templateId) {
|
||||||
|
usageStats[templateId] = (usageStats[templateId] || 0) + 1;
|
||||||
|
await StorageHelper.set('usageStats', usageStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUsageCount(templateId) {
|
||||||
|
return usageStats[templateId] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast уведомления
|
||||||
|
function showToast(message, duration = 3000) {
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast show';
|
||||||
|
toast.innerHTML = `
|
||||||
|
<i class="fa-solid fa-check-circle"></i>
|
||||||
|
<span class="toast-message">${message}</span>
|
||||||
|
`;
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем переопределения встроенных шаблонов
|
||||||
|
let builtinOverrides = await StorageHelper.get('builtinOverrides') || {};
|
||||||
|
|
||||||
// Объект с текстами для каждого элемента
|
// Объект с текстами для каждого элемента
|
||||||
const clipboardTexts = {
|
let clipboardTexts = {
|
||||||
"inWorkRu": "Коллеги, здравствуйте.\nПриняли Ваш запрос в работу. Сообщим по мере поступления информации.",
|
"inWorkRu": "Коллеги, здравствуйте.\nПриняли Ваш запрос в работу. Сообщим по мере поступления информации.",
|
||||||
"inWorkEn": "Dear customer,\nWe are working on your request.",
|
"inWorkEn": "Dear customer,\nWe are working on your request.",
|
||||||
"dialogueRu": "Коллеги, здравствуйте.\nУведомляем вас о том, что мы начали диалог с оператором по данному запросу. Мы ожидаем ответа и будем держать вас в курсе любых изменений.",
|
"dialogueRu": "Коллеги, здравствуйте.\nУведомляем вас о том, что мы начали диалог с оператором по данному запросу. Мы ожидаем ответа и будем держать вас в курсе любых изменений.",
|
||||||
@@ -59,11 +177,59 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
"HLREn": "Dear colleagues,\nPlease clarify the correctness of HLR statuses for the following subscriber numbers:",
|
"HLREn": "Dear colleagues,\nPlease clarify the correctness of HLR statuses for the following subscriber numbers:",
|
||||||
"managerTemplate": "Страна: \nОператор: \nmccMnc: \nСендер: \nКлиент: \nSource node: \nDestination node: "
|
"managerTemplate": "Страна: \nОператор: \nmccMnc: \nСендер: \nКлиент: \nSource node: \nDestination node: "
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Применяем переопределения
|
||||||
|
Object.keys(builtinOverrides).forEach(key => {
|
||||||
|
if (clipboardTexts.hasOwnProperty(key)) {
|
||||||
|
clipboardTexts[key] = builtinOverrides[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция для редактирования встроенных шаблонов
|
||||||
|
async function editBuiltinTemplate(templateId) {
|
||||||
|
const newText = prompt('Введите новый текст шаблона:', clipboardTexts[templateId]);
|
||||||
|
if (newText !== null && newText !== clipboardTexts[templateId]) {
|
||||||
|
clipboardTexts[templateId] = newText;
|
||||||
|
builtinOverrides[templateId] = newText;
|
||||||
|
await StorageHelper.set('builtinOverrides', builtinOverrides);
|
||||||
|
showToast('Встроенный шаблон обновлён!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчики для кнопок редактирования встроенных шаблонов
|
||||||
|
document.querySelectorAll('.template-edit-builtin').forEach(btn => {
|
||||||
|
const template = btn.closest('.template');
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
// Находим RU и EN кнопки
|
||||||
|
const ruBtn = template.querySelector('[id$="Ru"]');
|
||||||
|
const enBtn = template.querySelector('[id$="En"]');
|
||||||
|
|
||||||
|
if (ruBtn || enBtn) {
|
||||||
|
const templateName = template.querySelector('.template-title').textContent.trim();
|
||||||
|
const action = confirm(`Редактировать шаблон "${templateName}"?\nОК - Редактировать RU\nОтмена - Редактировать EN`);
|
||||||
|
|
||||||
|
if (action && ruBtn) {
|
||||||
|
editBuiltinTemplate(ruBtn.id);
|
||||||
|
} else if (!action && enBtn) {
|
||||||
|
editBuiltinTemplate(enBtn.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Функция для обработки кликов
|
// Функция для обработки кликов
|
||||||
function handleClipboardClick(buttonId) {
|
function handleClipboardClick(buttonId) {
|
||||||
if (clipboardTexts[buttonId]) {
|
if (clipboardTexts[buttonId]) {
|
||||||
navigator.clipboard.writeText(clipboardTexts[buttonId]);
|
navigator.clipboard.writeText(clipboardTexts[buttonId])
|
||||||
|
.then(() => {
|
||||||
|
updateUsageStats(buttonId);
|
||||||
|
const count = getUsageCount(buttonId);
|
||||||
|
showToast(`Текст скопирован! (Использований: ${count})`);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Не удалось скопировать текст: ', err);
|
||||||
|
showToast('Ошибка копирования');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,12 +287,12 @@ function createTemplate(title, RUText, ENText) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Обработчик для удаления
|
// Обработчик для удаления
|
||||||
deleteBtn.addEventListener('click', () => {
|
deleteBtn.addEventListener('click', async () => {
|
||||||
const templates = JSON.parse(localStorage.getItem('templates')) || [];
|
const templates = await StorageHelper.get('templates') || [];
|
||||||
const templateIndex = templates.findIndex(template => template.title === titleEl.innerText);
|
const templateIndex = templates.findIndex(template => template.title === titleEl.innerText);
|
||||||
if (templateIndex !== -1) {
|
if (templateIndex !== -1) {
|
||||||
templates.splice(templateIndex, 1);
|
templates.splice(templateIndex, 1);
|
||||||
localStorage.setItem('templates', JSON.stringify(templates));
|
await StorageHelper.set('templates', templates);
|
||||||
}
|
}
|
||||||
templateEl.remove();
|
templateEl.remove();
|
||||||
});
|
});
|
||||||
@@ -144,9 +310,18 @@ function createTemplate(title, RUText, ENText) {
|
|||||||
|
|
||||||
// Обработчик для копирования текста в буфер обмена
|
// Обработчик для копирования текста в буфер обмена
|
||||||
const handleCopyText = (e, key) => {
|
const handleCopyText = (e, key) => {
|
||||||
|
const templateId = `custom-${title}-${key}`;
|
||||||
navigator.clipboard.writeText(e.target.dataset[key])
|
navigator.clipboard.writeText(e.target.dataset[key])
|
||||||
.then(() => console.log(`Текст ${key} скопирован в буфер обмена`))
|
.then(() => {
|
||||||
.catch(err => console.error(`Не удалось скопировать текст ${key}: `, err));
|
updateUsageStats(templateId);
|
||||||
|
const count = getUsageCount(templateId);
|
||||||
|
showToast(`Текст скопирован! (Использований: ${count})`);
|
||||||
|
console.log(`Текст ${key} скопирован в буфер обмена`);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(`Не удалось скопировать текст ${key}: `, err);
|
||||||
|
showToast('Ошибка копирования');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
textRUEl.addEventListener('click', (e) => handleCopyText(e, 'ruText'));
|
textRUEl.addEventListener('click', (e) => handleCopyText(e, 'ruText'));
|
||||||
@@ -155,19 +330,19 @@ function createTemplate(title, RUText, ENText) {
|
|||||||
return templateEl;
|
return templateEl;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTemplateToLocalStorage(title, RUText, ENText) {
|
async function saveTemplateToLocalStorage(title, RUText, ENText) {
|
||||||
const template = { title, RUText, ENText };
|
const template = { title, RUText, ENText };
|
||||||
const templates = JSON.parse(localStorage.getItem('templates')) || [];
|
const templates = await StorageHelper.get('templates') || [];
|
||||||
templates.push(template);
|
templates.push(template);
|
||||||
localStorage.setItem('templates', JSON.stringify(templates));
|
await StorageHelper.set('templates', templates);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTemplateInLocalStorage(title, key, value) {
|
async function updateTemplateInLocalStorage(title, key, value) {
|
||||||
const templates = JSON.parse(localStorage.getItem('templates')) || [];
|
const templates = await StorageHelper.get('templates') || [];
|
||||||
const templateIndex = templates.findIndex(template => template.title === title);
|
const templateIndex = templates.findIndex(template => template.title === title);
|
||||||
if (templateIndex !== -1) {
|
if (templateIndex !== -1) {
|
||||||
templates[templateIndex][key] = value;
|
templates[templateIndex][key] = value;
|
||||||
localStorage.setItem('templates', JSON.stringify(templates));
|
await StorageHelper.set('templates', templates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +356,8 @@ addBtn.addEventListener('click', () => {
|
|||||||
saveTemplateToLocalStorage(title, RUText, ENText);
|
saveTemplateToLocalStorage(title, RUText, ENText);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.onload = function () {
|
window.onload = async function () {
|
||||||
const templates = JSON.parse(localStorage.getItem('templates')) || [];
|
const templates = await StorageHelper.get('templates') || [];
|
||||||
templates.forEach(template => {
|
templates.forEach(template => {
|
||||||
const el = createTemplate(template.title, template.RUText, template.ENText);
|
const el = createTemplate(template.title, template.RUText, template.ENText);
|
||||||
templatesEl.appendChild(el);
|
templatesEl.appendChild(el);
|
||||||
@@ -195,8 +370,8 @@ const themeToggle = document.getElementById('theme-toggle');
|
|||||||
const themeIcon = document.getElementById('theme-icon');
|
const themeIcon = document.getElementById('theme-icon');
|
||||||
|
|
||||||
// Применение сохраненной темы
|
// Применение сохраненной темы
|
||||||
const applySavedTheme = () => {
|
const applySavedTheme = async () => {
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = await StorageHelper.get('theme');
|
||||||
if (savedTheme === 'dark') {
|
if (savedTheme === 'dark') {
|
||||||
document.body.classList.add('dark-theme');
|
document.body.classList.add('dark-theme');
|
||||||
if (themeIcon) {
|
if (themeIcon) {
|
||||||
@@ -212,10 +387,10 @@ const applySavedTheme = () => {
|
|||||||
|
|
||||||
// Переключение темы
|
// Переключение темы
|
||||||
if (themeToggle && themeIcon) {
|
if (themeToggle && themeIcon) {
|
||||||
themeToggle.addEventListener('click', () => {
|
themeToggle.addEventListener('click', async () => {
|
||||||
document.body.classList.toggle('dark-theme');
|
document.body.classList.toggle('dark-theme');
|
||||||
const theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
|
const theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
|
||||||
localStorage.setItem('theme', theme); // Сохраняем выбранную тему
|
await StorageHelper.set('theme', theme); // Сохраняем выбранную тему
|
||||||
themeIcon.classList.replace(theme === 'dark' ? 'fa-moon' : 'fa-sun', theme === 'dark' ? 'fa-sun' : 'fa-moon');
|
themeIcon.classList.replace(theme === 'dark' ? 'fa-moon' : 'fa-sun', theme === 'dark' ? 'fa-sun' : 'fa-moon');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -224,13 +399,14 @@ if (themeToggle && themeIcon) {
|
|||||||
applySavedTheme();
|
applySavedTheme();
|
||||||
|
|
||||||
// Управление категориями
|
// Управление категориями
|
||||||
document.querySelectorAll('.category h3').forEach(function(header) {
|
document.querySelectorAll('.category h3').forEach(async function(header) {
|
||||||
const categoryId = header.textContent.trim(); // Уникальный идентификатор категории по названию
|
const categoryId = header.textContent.trim(); // Уникальный идентификатор категории по названию
|
||||||
const content = header.nextElementSibling; // Содержимое категории
|
const content = header.nextElementSibling; // Содержимое категории
|
||||||
const icon = header.querySelector('.toggle-icon'); // Иконка стрелки
|
const icon = header.querySelector('.toggle-icon'); // Иконка стрелки
|
||||||
|
|
||||||
// Проверка состояния категории при загрузке страницы
|
// Проверка состояния категории при загрузке страницы
|
||||||
const isCategoryOpen = localStorage.getItem(categoryId) === 'true';
|
const categoryStates = await StorageHelper.get('categoryStates') || {};
|
||||||
|
const isCategoryOpen = categoryStates[categoryId] === 'true';
|
||||||
|
|
||||||
if (isCategoryOpen) {
|
if (isCategoryOpen) {
|
||||||
content.classList.add('show');
|
content.classList.add('show');
|
||||||
@@ -245,12 +421,180 @@ document.querySelectorAll('.category h3').forEach(function(header) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Обработчик клика по заголовку категории
|
// Обработчик клика по заголовку категории
|
||||||
header.addEventListener('click', function() {
|
header.addEventListener('click', async function() {
|
||||||
content.classList.toggle('show'); // Переключаем видимость категории
|
content.classList.toggle('show'); // Переключаем видимость категории
|
||||||
icon.classList.toggle('fa-chevron-down');
|
icon.classList.toggle('fa-chevron-down');
|
||||||
icon.classList.toggle('fa-chevron-up');
|
icon.classList.toggle('fa-chevron-up');
|
||||||
// Сохраняем состояние категории как открытое или закрытое
|
// Сохраняем состояние категории как открытое или закрытое
|
||||||
localStorage.setItem(categoryId, content.classList.contains('show') ? 'true' : 'false');
|
const categoryStates = await StorageHelper.get('categoryStates') || {};
|
||||||
|
categoryStates[categoryId] = content.classList.contains('show') ? 'true' : 'false';
|
||||||
|
await StorageHelper.set('categoryStates', categoryStates);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Импорт/Экспорт шаблонов
|
||||||
|
const exportBtn = document.querySelector('.export-btn');
|
||||||
|
const importBtn = document.querySelector('.import-btn');
|
||||||
|
const importFileInput = document.getElementById('import-file-input');
|
||||||
|
|
||||||
|
if (exportBtn) {
|
||||||
|
exportBtn.addEventListener('click', async () => {
|
||||||
|
const templates = await StorageHelper.get('templates') || [];
|
||||||
|
const dataStr = JSON.stringify(templates, null, 2);
|
||||||
|
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `izipus-templates-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
showToast(`Экспортировано ${templates.length} шаблонов`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importBtn && importFileInput) {
|
||||||
|
importBtn.addEventListener('click', () => {
|
||||||
|
importFileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
importFileInput.addEventListener('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
try {
|
||||||
|
const importedTemplates = JSON.parse(event.target.result);
|
||||||
|
|
||||||
|
if (!Array.isArray(importedTemplates)) {
|
||||||
|
showToast('Неверный формат файла');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидация шаблонов
|
||||||
|
const validTemplates = importedTemplates.filter(t =>
|
||||||
|
t.title && typeof t.title === 'string' &&
|
||||||
|
t.RUText && typeof t.RUText === 'string' &&
|
||||||
|
t.ENText && typeof t.ENText === 'string'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validTemplates.length === 0) {
|
||||||
|
showToast('Нет валидных шаблонов для импорта');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем существующие шаблоны
|
||||||
|
const existingTemplates = await StorageHelper.get('templates') || [];
|
||||||
|
|
||||||
|
// Объединяем шаблоны (избегаем дубликатов по названию)
|
||||||
|
const existingTitles = new Set(existingTemplates.map(t => t.title));
|
||||||
|
const newTemplates = validTemplates.filter(t => !existingTitles.has(t.title));
|
||||||
|
|
||||||
|
if (newTemplates.length === 0) {
|
||||||
|
showToast('Все шаблоны уже существуют');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedTemplates = [...existingTemplates, ...newTemplates];
|
||||||
|
await StorageHelper.set('templates', mergedTemplates);
|
||||||
|
|
||||||
|
// Добавляем новые шаблоны на страницу
|
||||||
|
newTemplates.forEach(template => {
|
||||||
|
const el = createTemplate(template.title, template.RUText, template.ENText);
|
||||||
|
templatesEl.appendChild(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast(`Импортировано ${newTemplates.length} новых шаблонов`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка импорта:', error);
|
||||||
|
showToast('Ошибка при импорте файла');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сбрасываем input
|
||||||
|
importFileInput.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск по шаблонам
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', function(e) {
|
||||||
|
const searchTerm = e.target.value.toLowerCase().trim();
|
||||||
|
const categories = document.querySelectorAll('.category');
|
||||||
|
|
||||||
|
if (searchTerm === '') {
|
||||||
|
// Если поиск пуст, показываем все шаблоны
|
||||||
|
categories.forEach(category => {
|
||||||
|
category.classList.remove('hidden-by-search');
|
||||||
|
const templates = category.querySelectorAll('.template');
|
||||||
|
templates.forEach(template => {
|
||||||
|
template.classList.remove('hidden-by-search');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем по всем категориям
|
||||||
|
categories.forEach(category => {
|
||||||
|
const templates = category.querySelectorAll('.template');
|
||||||
|
let hasVisibleTemplates = false;
|
||||||
|
|
||||||
|
templates.forEach(template => {
|
||||||
|
// Получаем название шаблона
|
||||||
|
const titleEl = template.querySelector('.template-title, #template-title');
|
||||||
|
const title = titleEl ? titleEl.textContent.toLowerCase() : '';
|
||||||
|
|
||||||
|
// Получаем содержимое шаблонов (RU и EN)
|
||||||
|
const ruBtn = template.querySelector('[id$="Ru"], [data-ru-text]');
|
||||||
|
const enBtn = template.querySelector('[id$="En"], [data-en-text]');
|
||||||
|
|
||||||
|
let ruText = '';
|
||||||
|
let enText = '';
|
||||||
|
|
||||||
|
if (ruBtn) {
|
||||||
|
ruText = (ruBtn.dataset.ruText || clipboardTexts[ruBtn.id] || '').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enBtn) {
|
||||||
|
enText = (enBtn.dataset.enText || clipboardTexts[enBtn.id] || '').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем совпадение
|
||||||
|
const matches = title.includes(searchTerm) ||
|
||||||
|
ruText.includes(searchTerm) ||
|
||||||
|
enText.includes(searchTerm);
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
template.classList.remove('hidden-by-search');
|
||||||
|
hasVisibleTemplates = true;
|
||||||
|
} else {
|
||||||
|
template.classList.add('hidden-by-search');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Скрываем категорию, если в ней нет видимых шаблонов
|
||||||
|
if (hasVisibleTemplates) {
|
||||||
|
category.classList.remove('hidden-by-search');
|
||||||
|
// Открываем категорию при поиске
|
||||||
|
const content = category.querySelector('.category-content');
|
||||||
|
if (content && !content.classList.contains('show')) {
|
||||||
|
content.classList.add('show');
|
||||||
|
const icon = category.querySelector('.toggle-icon');
|
||||||
|
if (icon) {
|
||||||
|
icon.classList.replace('fa-chevron-down', 'fa-chevron-up');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
category.classList.add('hidden-by-search');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user