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",
|
||||
"version": "0.3.1",
|
||||
"version": "0.4.0",
|
||||
"manifest_version": 3,
|
||||
"description": "Lzipus - assists your workflow by simplifying interactions.",
|
||||
"homepage_url": "https://git.gorshenin.info/Dgors03/Answer_Templates",
|
||||
|
||||
@@ -8,6 +8,28 @@ body {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -18,26 +40,17 @@ div.container {
|
||||
div.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding: 6px;
|
||||
font-family: Helvetica, Arial, sans-serif; /* Более минималистичный шрифт для категорий */
|
||||
font-size: 16px; /* Уменьшен размер шрифта */
|
||||
font-weight: normal; /* Уменьшен вес шрифта для минималистичного вида */
|
||||
color: #333; /* Цвет шрифта для категорий */
|
||||
}
|
||||
|
||||
i {
|
||||
width: 20px;
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
button.template-add,
|
||||
button.template-edit,
|
||||
button.template-delete {
|
||||
@@ -49,19 +62,19 @@ button.template-delete {
|
||||
cursor: pointer;
|
||||
font-size: 14px; /* Меньший размер текста */
|
||||
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-edit:hover,
|
||||
button.template-delete:hover {
|
||||
background-color: #abcef5; /* Подсветка фоном */
|
||||
color: #fff; /* Белый цвет текста */
|
||||
border-color: #88aee5; /* Немного темнее рамка */
|
||||
transform: scale(1.05); /* Увеличение кнопки при наведении */
|
||||
}
|
||||
|
||||
/* Эффект при фокусе */
|
||||
button.template-add:focus,
|
||||
button.template-edit:focus,
|
||||
button.template-delete:focus {
|
||||
@@ -69,6 +82,7 @@ button.template-delete:focus {
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2); /* Легкая тень для фокуса */
|
||||
}
|
||||
|
||||
/* Шаблоны */
|
||||
div.templates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -83,12 +97,16 @@ span.template {
|
||||
justify-content: space-between;
|
||||
height: fit-content;
|
||||
padding: 8px 7px;
|
||||
justify-content: space-between;
|
||||
height: fit-content;
|
||||
padding: 8px 7px;
|
||||
border: 1px solid #8888c6; /* Цвет рамки совпадает с рамкой категории */
|
||||
border-radius: 8px; /* Скругленные углы */
|
||||
overflow: hidden;
|
||||
width: 100%; /* Шаблоны должны растягиваться на всю ширину */
|
||||
box-sizing: border-box; /* Чтобы паддинги не влияли на размер */
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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>
|
||||
|
||||
<body>
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
<button id="theme-toggle" class="theme-btn">
|
||||
<i id="theme-icon" class="fa-moon fa-solid"></i>
|
||||
</button>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<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 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">
|
||||
<h3 class="font-monospace">
|
||||
Промежуточные ответы
|
||||
@@ -28,12 +46,12 @@
|
||||
|
||||
</h3>
|
||||
<div class="category-content">
|
||||
<span class="template">
|
||||
<span class="template" data-builtin-id="inWork">
|
||||
<span class="template-title font-monospace">Приняли в работу</span>
|
||||
<span class="buttons">
|
||||
<span class="btn btn-light" id="inWorkRu">RU</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 class="template">
|
||||
@@ -277,7 +295,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</1--div>
|
||||
</div>
|
||||
<script src="popup.js"></script>
|
||||
</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Приняли Ваш запрос в работу. Сообщим по мере поступления информации.",
|
||||
"inWorkEn": "Dear customer,\nWe are working on your request.",
|
||||
"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:",
|
||||
"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) {
|
||||
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', () => {
|
||||
const templates = JSON.parse(localStorage.getItem('templates')) || [];
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
const templates = await StorageHelper.get('templates') || [];
|
||||
const templateIndex = templates.findIndex(template => template.title === titleEl.innerText);
|
||||
if (templateIndex !== -1) {
|
||||
templates.splice(templateIndex, 1);
|
||||
localStorage.setItem('templates', JSON.stringify(templates));
|
||||
await StorageHelper.set('templates', templates);
|
||||
}
|
||||
templateEl.remove();
|
||||
});
|
||||
@@ -144,9 +310,18 @@ function createTemplate(title, RUText, ENText) {
|
||||
|
||||
// Обработчик для копирования текста в буфер обмена
|
||||
const handleCopyText = (e, key) => {
|
||||
const templateId = `custom-${title}-${key}`;
|
||||
navigator.clipboard.writeText(e.target.dataset[key])
|
||||
.then(() => console.log(`Текст ${key} скопирован в буфер обмена`))
|
||||
.catch(err => console.error(`Не удалось скопировать текст ${key}: `, err));
|
||||
.then(() => {
|
||||
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'));
|
||||
@@ -155,19 +330,19 @@ function createTemplate(title, RUText, ENText) {
|
||||
return templateEl;
|
||||
}
|
||||
|
||||
function saveTemplateToLocalStorage(title, RUText, ENText) {
|
||||
async function saveTemplateToLocalStorage(title, RUText, ENText) {
|
||||
const template = { title, RUText, ENText };
|
||||
const templates = JSON.parse(localStorage.getItem('templates')) || [];
|
||||
const templates = await StorageHelper.get('templates') || [];
|
||||
templates.push(template);
|
||||
localStorage.setItem('templates', JSON.stringify(templates));
|
||||
await StorageHelper.set('templates', templates);
|
||||
}
|
||||
|
||||
function updateTemplateInLocalStorage(title, key, value) {
|
||||
const templates = JSON.parse(localStorage.getItem('templates')) || [];
|
||||
async function updateTemplateInLocalStorage(title, key, value) {
|
||||
const templates = await StorageHelper.get('templates') || [];
|
||||
const templateIndex = templates.findIndex(template => template.title === title);
|
||||
if (templateIndex !== -1) {
|
||||
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);
|
||||
});
|
||||
|
||||
window.onload = function () {
|
||||
const templates = JSON.parse(localStorage.getItem('templates')) || [];
|
||||
window.onload = async function () {
|
||||
const templates = await StorageHelper.get('templates') || [];
|
||||
templates.forEach(template => {
|
||||
const el = createTemplate(template.title, template.RUText, template.ENText);
|
||||
templatesEl.appendChild(el);
|
||||
@@ -195,8 +370,8 @@ const themeToggle = document.getElementById('theme-toggle');
|
||||
const themeIcon = document.getElementById('theme-icon');
|
||||
|
||||
// Применение сохраненной темы
|
||||
const applySavedTheme = () => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const applySavedTheme = async () => {
|
||||
const savedTheme = await StorageHelper.get('theme');
|
||||
if (savedTheme === 'dark') {
|
||||
document.body.classList.add('dark-theme');
|
||||
if (themeIcon) {
|
||||
@@ -212,10 +387,10 @@ const applySavedTheme = () => {
|
||||
|
||||
// Переключение темы
|
||||
if (themeToggle && themeIcon) {
|
||||
themeToggle.addEventListener('click', () => {
|
||||
themeToggle.addEventListener('click', async () => {
|
||||
document.body.classList.toggle('dark-theme');
|
||||
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');
|
||||
});
|
||||
}
|
||||
@@ -224,13 +399,14 @@ if (themeToggle && themeIcon) {
|
||||
applySavedTheme();
|
||||
|
||||
// Управление категориями
|
||||
document.querySelectorAll('.category h3').forEach(function(header) {
|
||||
document.querySelectorAll('.category h3').forEach(async function(header) {
|
||||
const categoryId = header.textContent.trim(); // Уникальный идентификатор категории по названию
|
||||
const content = header.nextElementSibling; // Содержимое категории
|
||||
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) {
|
||||
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'); // Переключаем видимость категории
|
||||
icon.classList.toggle('fa-chevron-down');
|
||||
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