2 Commits

Author SHA1 Message Date
c53f174959 test 2025-10-27 23:39:55 +03:00
5c11d21290 main 2025-10-27 23:38:35 +03:00
6 changed files with 804 additions and 48 deletions

101
lzipus/CHANGELOG.md Normal file
View 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
View 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

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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');
}
});
});
}
}); });