add update button

This commit is contained in:
2026-01-31 04:21:43 +03:00
parent 9c28cd66e3
commit 9d39a4c384
17 changed files with 2209 additions and 304 deletions

72
README.md Normal file
View File

@@ -0,0 +1,72 @@
# Izipus — менеджер шаблонов ответов
Расширение для браузеров на базе Chromium, которое собирает текстовые шаблоны в одном месте и позволяет копировать их в буфер обмена одним кликом.
## Возможности
- **Категории шаблонов** — шаблоны сгруппированы по сценариям (ответы клиентам, запросы партнёрам, коды ошибок и т.д.). Состояние разворота категорий сохраняется
- **Быстрое копирование** — один клик копирует текст в буфер. `Alt+1...0` для первых десяти шаблонов, `Alt+Shift+1...0` для английской версии
- **Поиск** — быстрый поиск по названиям и содержимому шаблонов
- **Пользовательские шаблоны** — создание, редактирование и удаление своих шаблонов. Сохраняются в `chrome.storage.sync`
- **Экспорт/импорт** — экспорт всех шаблонов в JSON, импорт через drag&drop, выбор файла или вставку текста
- **Темы** — тёмная, светлая и системная тема с переключением в один клик
- **Проверка обновлений** — кнопка в шапке запрашивает версию из репозитория (Gitea) и при наличии новой версии открывает страницу репозитория для ручной загрузки
## Обновление расширения (вручную)
Браузер **не разрешает** расширению самостоятельно скачивать и устанавливать себе новую версию с произвольного URL (Gitea, GitHub и т.п.) — это ограничение безопасности. Поэтому возможен только такой сценарий:
1. В расширении нажмите кнопку **«Проверить обновления»** (иконка обновления в шапке).
2. При первом нажатии браузер запросит доступ к репозиторию — разрешите.
3. Если доступна новая версия, откроется вкладка с репозиторием. Скачайте архив (например, **Clone****Download ZIP** или страница релизов).
4. Распакуйте архив поверх папки, в которой установлено расширение (замените файлы).
5. Откройте `chrome://extensions` и нажмите **Обновить** (или перезагрузите расширение).
**Если расширение установлено из клона репозитория (git clone):** можно обновляться скриптом в корне проекта:
- **Windows:** двойной клик по `update.bat`.
- **Linux / macOS:** в терминале выполните `chmod +x update.sh` (один раз), затем `./update.sh`.
После обновления перезагрузите расширение в `chrome://extensions`.
Версия для сравнения берётся из `manifest.json` в ветке `main` (или `master`) репозитория. Убедитесь, что в репозитории в этой ветке поле `version` в `manifest.json` обновлено.
## Установка
1. Скачайте или клонируйте репозиторий
2. Откройте `chrome://extensions` в браузере
3. Включите "Режим разработчика"
4. Нажмите "Загрузить распакованное" и укажите папку проекта
5. Иконка расширения появится на панели инструментов
## Структура проекта
```
src/
├── popup.html # HTML разметка
├── popup.js # Главный файл инициализации
├── popup.css # Стили
├── templates.json # Предустановленные шаблоны
└── js/ # Модули приложения
├── constants.js # Константы
├── storage.js # Работа с хранилищем
├── clipboard.js # Буфер обмена
├── templates.js # Валидация и работа с шаблонами
├── ui.js # Модальные окна и категории
├── hotkeys.js # Горячие клавиши
├── search.js # Поиск
├── theme.js # Управление темой
└── update-check.js # Проверка версии в репозитории (Gitea)
```
## Разработка
- **Редактирование шаблонов** — тексты в `src/templates.json`, после изменений перезагрузите расширение
- **Добавление шаблонов** — создайте элемент в `popup.html` с нужным `id` и добавьте текст в `templates.json`
- **Стили** — все стили в `src/popup.css`, используется Bootstrap для базовых компонентов
## Ограничения
- Лимит `chrome.storage.sync` — 100 КБ на расширение. При превышении экспортируйте данные
- Предустановленные шаблоны редактируются только через JSON/HTML
- Только ручная установка, публикация в магазинах не планируется
- Обновление расширения только вручную: расширение не может само скачать и установить новую версию; кнопка «Проверить обновления» лишь показывает, есть ли новая версия в репозитории, и открывает страницу для загрузки

View File

@@ -21,6 +21,9 @@
"storage", "storage",
"cookies" "cookies"
], ],
"optional_host_permissions": [
"https://git.gorshenin.info/*"
],
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "dmitriy.gorshenin1@gmail.com" "id": "dmitriy.gorshenin1@gmail.com"

86
src/js/clipboard.js Normal file
View File

@@ -0,0 +1,86 @@
export const loadClipboardTexts = async () => {
try {
const resourceUrl = typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getURL
? chrome.runtime.getURL('src/templates.json')
: 'src/templates.json';
const response = await fetch(resourceUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return data.clipboardTexts || data;
} catch (error) {
console.error('Не удалось загрузить предустановленные шаблоны:', error);
return {};
}
};
export const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (error) {
console.error('Не удалось скопировать текст:', error);
return false;
}
};
const normalizePredefinedId = (id) => {
if (!id) return id;
return id.replace(/Ru$|En$|Eng$/, '');
};
export const initializeClipboardButtons = (clipboardTexts, favorites = [], onToggleFavorite = null) => {
const processedTemplates = new Set();
Object.keys(clipboardTexts).forEach(buttonId => {
const button = document.getElementById(buttonId);
if (!button) {
return;
}
const templateEl = button.closest('.template');
if (!templateEl) {
return;
}
const normalizedId = normalizePredefinedId(buttonId);
const isFirstForTemplate = !processedTemplates.has(normalizedId);
if (isFirstForTemplate) {
processedTemplates.add(normalizedId);
}
const clipboardValue = clipboardTexts[buttonId];
if (clipboardValue && templateEl) {
const existingSearch = templateEl.dataset.searchText || templateEl.textContent.toLowerCase();
templateEl.dataset.searchText = `${existingSearch} ${clipboardValue.toLowerCase()}`.trim();
const buttonLabel = button.textContent.trim().toLowerCase();
if (buttonLabel === 'ru') {
templateEl.dataset.templateRu = clipboardValue;
} else if (buttonLabel === 'eng' || buttonLabel === 'en') {
templateEl.dataset.templateEn = clipboardValue;
}
}
button.addEventListener('click', () => {
if (clipboardTexts[buttonId]) {
copyToClipboard(clipboardTexts[buttonId]);
}
});
if (isFirstForTemplate && onToggleFavorite && !templateEl.querySelector('.template-favorite')) {
const isFavorite = favorites.includes(normalizedId);
const favoriteBtn = document.createElement('button');
favoriteBtn.className = 'template-favorite btn';
favoriteBtn.type = 'button';
favoriteBtn.title = isFavorite ? 'Удалить из избранного' : 'Добавить в избранное';
favoriteBtn.innerHTML = `<i class="fa-${isFavorite ? 'solid' : 'regular'} fa-star"></i>`;
favoriteBtn.addEventListener('click', async (e) => {
e.stopPropagation();
const currentState = templateEl.dataset.favorite === 'true';
await onToggleFavorite(normalizedId, !currentState);
});
templateEl.insertBefore(favoriteBtn, templateEl.firstChild);
templateEl.dataset.templateId = normalizedId;
templateEl.dataset.favorite = isFavorite ? 'true' : 'false';
}
});
};

12
src/js/constants.js Normal file
View File

@@ -0,0 +1,12 @@
export const MIN_TITLE_LENGTH = 3;
export const MIN_TEXT_LENGTH = 5;
export const HOTKEY_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
/**
* Базовый URL репозитория для проверки обновлений и страницы «Скачать».
* Поддерживаются Gitea и GitLab — по хосту выбирается формат raw-URL для manifest.json.
* Примеры:
* Gitea: https://git.gorshenin.info/Dgors03/Answer_Templates
* GitLab: https://gitlab.com/username/izipus
*/
export const REPO_BASE = 'https://git.gorshenin.info/Dgors03/Answer_Templates';

69
src/js/hotkeys.js Normal file
View File

@@ -0,0 +1,69 @@
import { HOTKEY_KEYS } from './constants.js';
import { copyToClipboard } from './clipboard.js';
export class HotkeyManager {
constructor() {
this.assignedHotkeys = new Map();
this.setupGlobalHandler();
}
updateAssignments() {
this.assignedHotkeys.clear();
document.querySelectorAll('.hotkey-badge').forEach(badge => badge.remove());
const visibleTemplates = Array.from(document.querySelectorAll('.template')).filter(template => {
return !template.classList.contains('filtered-out') && template.offsetParent !== null;
});
visibleTemplates.forEach((template, index) => {
if (index >= HOTKEY_KEYS.length) {
delete template.dataset.hotkey;
return;
}
const key = HOTKEY_KEYS[index];
this.assignedHotkeys.set(key, template);
template.dataset.hotkey = key;
const titleNode = template.querySelector('.template-title');
if (titleNode) {
const badge = document.createElement('span');
badge.className = 'hotkey-badge';
badge.textContent = `Alt+${key}`;
titleNode.prepend(badge);
}
});
}
setupGlobalHandler() {
document.addEventListener('keydown', (event) => {
if (!event.altKey || event.ctrlKey || event.metaKey) {
return;
}
const key = event.key.toLowerCase();
if (!HOTKEY_KEYS.includes(key)) {
return;
}
const template = this.assignedHotkeys.get(key);
if (!template) {
return;
}
event.preventDefault();
const preferEnglish = event.shiftKey;
const ruText = template.dataset.templateRu || '';
const enText = template.dataset.templateEn || '';
const textToCopy = preferEnglish && enText ? enText : (ruText || enText);
if (!textToCopy) {
console.warn('Нет текста для копирования по горячей клавише.');
return;
}
copyToClipboard(textToCopy)
.then(() => {
console.log(`Текст скопирован по Alt+${key}${preferEnglish ? ' (ENG)' : ' (RU)'}`);
})
.catch(err => console.error('Не удалось скопировать текст по горячей клавише:', err));
});
}
}

54
src/js/search.js Normal file
View File

@@ -0,0 +1,54 @@
export class SearchManager {
constructor(categoryManager, hotkeyManager) {
this.categoryManager = categoryManager;
this.hotkeyManager = hotkeyManager;
this.searchInput = document.querySelector('.template-search');
this.init();
}
init() {
if (this.searchInput) {
this.searchInput.addEventListener('input', (event) => {
this.filter(event.target.value);
});
}
}
filter(query) {
const normalized = query.trim().toLowerCase();
const controllers = this.categoryManager.getControllers();
if (!normalized) {
controllers.forEach(controller => {
controller.category.classList.remove('filtered-out');
controller.content.querySelectorAll('.template').forEach(template => {
template.classList.remove('filtered-out');
});
const storedState = localStorage.getItem(controller.categoryId);
const shouldOpen = storedState === null
? (controller.category.dataset.category === 'custom' || controller.category.dataset.category === 'favorites')
: storedState === 'true';
controller.setOpenState(shouldOpen);
});
this.hotkeyManager.updateAssignments();
return;
}
controllers.forEach(controller => {
let hasMatches = false;
controller.content.querySelectorAll('.template').forEach(template => {
const searchSource = (template.dataset.searchText || template.textContent || '').toLowerCase();
const matches = searchSource.includes(normalized);
template.classList.toggle('filtered-out', !matches);
if (matches) {
hasMatches = true;
}
});
controller.category.classList.toggle('filtered-out', !hasMatches);
if (hasMatches) {
controller.setOpenState(true);
}
});
this.hotkeyManager.updateAssignments();
}
}

142
src/js/storage.js Normal file
View File

@@ -0,0 +1,142 @@
export const STORAGE_KEY = 'templates';
export const FAVORITES_KEY = 'favorites';
export const createStorageProvider = () => {
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
return {
async get() {
return new Promise((resolve, reject) => {
chrome.storage.sync.get([STORAGE_KEY], (result) => {
if (chrome.runtime && chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve(result[STORAGE_KEY] || []);
});
});
},
async set(value) {
return new Promise((resolve, reject) => {
chrome.storage.sync.set({ [STORAGE_KEY]: value }, () => {
if (chrome.runtime && chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
resolve();
});
});
}
};
}
return {
async get() {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
},
async set(value) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
}
};
};
export class TemplateStorage {
constructor() {
this.provider = createStorageProvider();
this.templates = [];
}
async load() {
try {
this.templates = await this.provider.get();
} catch (error) {
console.error('Не удалось загрузить пользовательские шаблоны:', error);
this.templates = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
}
return this.templates;
}
async save() {
try {
await this.provider.set(this.templates);
} catch (error) {
console.error('Не удалось сохранить пользовательские шаблоны:', error);
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.templates));
}
}
add(template) {
this.templates.push(template);
return this.save();
}
update(id, key, value) {
const templateIndex = this.templates.findIndex(t => t.id === id);
if (templateIndex !== -1) {
this.templates[templateIndex][key] = value;
return this.save();
}
}
remove(id) {
this.templates = this.templates.filter(t => t.id !== id);
return this.save();
}
getAll() {
return [...this.templates];
}
setTemplates(templates) {
this.templates = templates;
return this.save();
}
async getFavorites() {
try {
const result = await this.provider.get();
if (typeof result === 'object' && !Array.isArray(result) && result.favorites) {
return result.favorites;
}
const favoritesData = typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync
? await new Promise((resolve) => {
chrome.storage.sync.get([FAVORITES_KEY], (result) => {
resolve(result[FAVORITES_KEY] || []);
});
})
: JSON.parse(localStorage.getItem(FAVORITES_KEY)) || [];
return favoritesData;
} catch (error) {
return JSON.parse(localStorage.getItem(FAVORITES_KEY)) || [];
}
}
async saveFavorites(favorites) {
try {
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
await new Promise((resolve) => {
chrome.storage.sync.set({ [FAVORITES_KEY]: favorites }, () => resolve());
});
} else {
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
}
} catch (error) {
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
}
}
async toggleFavorite(templateId, isFavorite) {
const favorites = await this.getFavorites();
if (isFavorite) {
if (!favorites.includes(templateId)) {
favorites.push(templateId);
}
} else {
const index = favorites.indexOf(templateId);
if (index > -1) {
favorites.splice(index, 1);
}
}
await this.saveFavorites(favorites);
return favorites;
}
}

324
src/js/templates.js Normal file
View File

@@ -0,0 +1,324 @@
import { MIN_TITLE_LENGTH, MIN_TEXT_LENGTH } from './constants.js';
import { copyToClipboard } from './clipboard.js';
export const generateTemplateId = () => {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
return `tpl-${Date.now()}-${Math.random().toString(16).slice(2)}`;
};
export const isTitleUnique = (title, templates, ignoreId = null) => {
const normalized = title.toLowerCase();
return !templates.some(template =>
template.title.toLowerCase() === normalized && template.id !== ignoreId
);
};
export const validateTitle = (rawTitle, templates, ignoreId = null) => {
const title = (rawTitle || '').trim();
if (title.length < MIN_TITLE_LENGTH) {
alert(`Название должно содержать не менее ${MIN_TITLE_LENGTH} символов.`);
return null;
}
if (!isTitleUnique(title, templates, ignoreId)) {
alert('Шаблон с таким названием уже существует. Пожалуйста, выберите другое название.');
return null;
}
return title;
};
export const validateText = (rawText, isOptional = false) => {
const text = (rawText || '').trim();
if (!text.length && isOptional) {
return '';
}
if (text.length < MIN_TEXT_LENGTH) {
alert(`Текст должен содержать не менее ${MIN_TEXT_LENGTH} символов.`);
return null;
}
return text;
};
export const ensureUniqueTitle = (title, usedTitles) => {
let candidate = title;
let suffix = 1;
while (usedTitles.has(candidate.toLowerCase())) {
candidate = `${title} (${suffix})`;
suffix += 1;
}
usedTitles.add(candidate.toLowerCase());
return candidate;
};
export const sanitizeImportedTemplate = (rawTemplate, usedIds, usedTitles) => {
if (!rawTemplate || typeof rawTemplate !== 'object') {
throw new Error('Неверная структура шаблона.');
}
const rawTitle = typeof rawTemplate.title === 'string' ? rawTemplate.title.trim() : '';
if (rawTitle.length < MIN_TITLE_LENGTH) {
throw new Error(`Название "${rawTitle}" слишком короткое.`);
}
const title = ensureUniqueTitle(rawTitle, usedTitles);
const ruTextRaw = typeof rawTemplate.RUText === 'string' ? rawTemplate.RUText.trim() : '';
if (ruTextRaw.length < MIN_TEXT_LENGTH) {
throw new Error(`Текст RU для "${title}" короче ${MIN_TEXT_LENGTH} символов.`);
}
const enTextRaw = typeof rawTemplate.ENText === 'string' ? rawTemplate.ENText.trim() : '';
if (enTextRaw && enTextRaw.length < MIN_TEXT_LENGTH) {
throw new Error(`Текст EN для "${title}" короче ${MIN_TEXT_LENGTH} символов.`);
}
let id = typeof rawTemplate.id === 'string' && rawTemplate.id.trim()
? rawTemplate.id
: generateTemplateId();
while (usedIds.has(id)) {
id = generateTemplateId();
}
usedIds.add(id);
return {
id,
title,
RUText: ruTextRaw,
ENText: enTextRaw
};
};
export const createTemplateElement = ({ id, title, RUText, ENText, favorite = false }, callbacks) => {
const { onUpdate, onDelete, onHotkeyUpdate, onToggleFavorite } = callbacks;
const updateSearchDataset = (templateEl, titleEl, textRUEl, textENEl) => {
const ru = textRUEl.dataset.ruText || '';
const en = textENEl.dataset.enText || '';
const combined = `${titleEl.textContent} ${ru} ${en}`.toLowerCase();
templateEl.dataset.searchText = combined;
templateEl.dataset.templateRu = ru;
if (en) {
templateEl.dataset.templateEn = en;
} else {
delete templateEl.dataset.templateEn;
}
};
const templateEl = document.createElement('span');
templateEl.dataset.templateId = id;
templateEl.classList.add('template');
templateEl.setAttribute('tabindex', '0');
templateEl.innerHTML = `
<span class="font-monospace template-title"></span>
<button class="template-favorite btn" type="button" title="Добавить в избранное">
<i class="fa-${favorite ? 'solid' : 'regular'} fa-star"></i>
</button>
<div class="bigFormochka">
<div class="formochka">
<input class="hidden form-control-sm template-title-input" />
<textarea class="hidden form-control-lg border-2 template-textarea-ru" rows="1" cols="30.5"></textarea>
<textarea class="hidden form-control-lg border-2 template-textarea-en" rows="1" cols="30.5"></textarea>
</div>
<span class="buttonsLong">
<span class="btn btn-light template-ru-text" data-ru-text="">RU</span>
<span class="btn btn-light template-en-text" data-en-text="">ENG</span>
<button class="template-edit btn" type="button"><i class="fa-solid fa-pen-to-square"></i></button>
<button class="template-delete btn" type="button"><i class="fa-solid fa-trash"></i></button>
</span>
</div>
`;
const titleEl = templateEl.querySelector('.template-title');
const favoriteBtn = templateEl.querySelector('.template-favorite');
const favoriteIcon = favoriteBtn?.querySelector('i');
const textRUEl = templateEl.querySelector('.template-ru-text');
const textENEl = templateEl.querySelector('.template-en-text');
const titleInputEl = templateEl.querySelector('.template-title-input');
const textRUInputEl = templateEl.querySelector('.template-textarea-ru');
const textENInputEl = templateEl.querySelector('.template-textarea-en');
const editBtn = templateEl.querySelector('.template-edit');
const deleteBtn = templateEl.querySelector('.template-delete');
const updateFavoriteIcon = (isFavorite) => {
if (favoriteIcon) {
favoriteIcon.className = `fa-${isFavorite ? 'solid' : 'regular'} fa-star`;
favoriteBtn.title = isFavorite ? 'Удалить из избранного' : 'Добавить в избранное';
}
templateEl.dataset.favorite = isFavorite ? 'true' : 'false';
};
const setInitialValues = () => {
titleEl.textContent = title;
textRUEl.dataset.ruText = RUText;
textRUEl.textContent = 'RU';
textENEl.dataset.enText = ENText;
textENEl.textContent = 'ENG';
titleInputEl.value = title;
textRUInputEl.value = RUText;
textENInputEl.value = ENText;
updateFavoriteIcon(favorite);
updateSearchDataset(templateEl, titleEl, textRUEl, textENEl);
};
setInitialValues();
if (favoriteBtn && onToggleFavorite) {
favoriteBtn.addEventListener('click', async (e) => {
e.stopPropagation();
const currentState = templateEl.dataset.favorite === 'true';
const newFavoriteState = !currentState;
await onToggleFavorite(id, newFavoriteState);
updateFavoriteIcon(newFavoriteState);
});
}
const toggleVisibility = (...elements) => {
elements.forEach(el => el.classList.toggle('hidden'));
};
editBtn.addEventListener('click', () => {
toggleVisibility(titleEl, textRUEl, textENEl, titleInputEl, textRUInputEl, textENInputEl);
if (!titleInputEl.classList.contains('hidden')) {
titleInputEl.focus();
}
});
deleteBtn.addEventListener('click', async () => {
await onDelete(id);
templateEl.remove();
if (onHotkeyUpdate) {
onHotkeyUpdate();
}
});
const handleTitleChange = async (event) => {
const templates = callbacks.getTemplates();
const newValue = validateTitle(event.target.value, templates, id);
if (!newValue) {
event.target.value = titleEl.textContent;
return;
}
titleEl.textContent = newValue;
event.target.value = newValue;
await onUpdate(id, 'title', newValue);
updateSearchDataset(templateEl, titleEl, textRUEl, textENEl);
if (onHotkeyUpdate) {
onHotkeyUpdate();
}
};
const handleTextChange = async (event, buttonEl, datasetKey, storageKey, isOptional = false) => {
const newValue = validateText(event.target.value, isOptional);
if (newValue === null) {
event.target.value = buttonEl.dataset[datasetKey] || '';
return;
}
buttonEl.dataset[datasetKey] = newValue;
await onUpdate(id, storageKey, newValue);
updateSearchDataset(templateEl, titleEl, textRUEl, textENEl);
};
titleInputEl.addEventListener('change', async (event) => {
await handleTitleChange(event);
});
textRUInputEl.addEventListener('change', async (e) => {
await handleTextChange(e, textRUEl, 'ruText', 'RUText');
});
textENInputEl.addEventListener('change', async (e) => {
await handleTextChange(e, textENEl, 'enText', 'ENText', true);
});
const handleCopyText = async (event, key) => {
const text = event.target.dataset[key];
if (!text) {
console.warn('Нет текста для копирования.');
return;
}
await copyToClipboard(text);
};
textRUEl.addEventListener('click', (e) => handleCopyText(e, 'ruText'));
textENEl.addEventListener('click', (e) => handleCopyText(e, 'enText'));
return templateEl;
};
export const downloadJsonFile = async (data, filename) => {
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const objectUrl = URL.createObjectURL(blob);
const revokeLater = () => setTimeout(() => URL.revokeObjectURL(objectUrl), 5000);
if (typeof chrome !== 'undefined' && chrome.downloads && chrome.downloads.download) {
try {
await new Promise((resolve, reject) => {
chrome.downloads.download({
url: objectUrl,
filename,
saveAs: false
}, (downloadId) => {
if (chrome.runtime && chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
return;
}
if (!downloadId) {
reject(new Error('Download id is empty'));
return;
}
resolve(downloadId);
});
});
} finally {
revokeLater();
}
return;
}
try {
const downloadLink = document.createElement('a');
downloadLink.href = objectUrl;
downloadLink.download = filename;
downloadLink.rel = 'noopener';
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
} finally {
revokeLater();
}
};
export const importTemplatesFromText = async (jsonText, templateStorage) => {
let parsed;
try {
parsed = JSON.parse(jsonText);
} catch (error) {
throw new Error('Неверный формат JSON.');
}
const templates = Array.isArray(parsed) ? parsed : (parsed.templates || []);
if (!Array.isArray(templates)) {
throw new Error('Ожидается массив шаблонов или объект с полем "templates".');
}
const usedIds = new Set(templateStorage.getAll().map(t => t.id));
const usedTitles = new Set(templateStorage.getAll().map(t => t.title.toLowerCase()));
const sanitized = templates.map(rawTemplate => {
try {
return sanitizeImportedTemplate(rawTemplate, usedIds, usedTitles);
} catch (error) {
throw new Error(`Ошибка при обработке шаблона: ${error.message}`);
}
});
const existingTemplates = templateStorage.getAll();
templateStorage.setTemplates([...existingTemplates, ...sanitized]);
return sanitized;
};

90
src/js/theme.js Normal file
View File

@@ -0,0 +1,90 @@
export class ThemeManager {
constructor() {
this.themeToggle = document.getElementById('theme-toggle');
this.themeIcon = document.getElementById('theme-icon');
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.currentThemeSetting = localStorage.getItem('theme') || 'system';
this.themeLabels = {
dark: 'тёмная',
light: 'светлая',
system: 'системная'
};
this.themeIcons = {
dark: 'fa-sun',
light: 'fa-moon',
system: 'fa-circle-half-stroke'
};
this.init();
}
init() {
if (this.themeToggle) {
this.themeToggle.addEventListener('click', () => {
const nextTheme = this.getNextTheme(this.currentThemeSetting);
this.applyTheme(nextTheme);
});
}
const handleSystemThemeChange = () => {
if (this.currentThemeSetting === 'system') {
this.applyTheme('system', { persist: false });
}
};
if (typeof this.mediaQuery.addEventListener === 'function') {
this.mediaQuery.addEventListener('change', handleSystemThemeChange);
} else if (typeof this.mediaQuery.addListener === 'function') {
this.mediaQuery.addListener(handleSystemThemeChange);
}
this.applyTheme(this.currentThemeSetting, { persist: false });
}
getNextTheme(theme) {
if (theme === 'dark') return 'light';
if (theme === 'light') return 'system';
return 'dark';
}
resolveEffectiveTheme(theme) {
if (theme === 'system') {
return this.mediaQuery.matches ? 'dark' : 'light';
}
return theme;
}
updateToggleMeta(theme) {
if (!this.themeToggle) {
return;
}
const applied = this.resolveEffectiveTheme(theme);
const currentLabel = theme === 'system'
? `${this.themeLabels[theme]} · сейчас ${this.themeLabels[applied]}`
: this.themeLabels[theme];
const nextTheme = this.getNextTheme(theme);
const nextLabel = this.themeLabels[nextTheme];
const description = `Тема: ${currentLabel}. Клик — ${nextLabel}.`;
this.themeToggle.setAttribute('title', description);
this.themeToggle.setAttribute('aria-label', description);
}
applyTheme(theme, { persist = true } = {}) {
this.currentThemeSetting = theme;
const effective = this.resolveEffectiveTheme(theme);
document.body.classList.toggle('dark-theme', effective === 'dark');
if (this.themeIcon) {
const iconClass = this.themeIcons[theme === 'system' ? 'system' : effective];
this.themeIcon.className = `fa-solid ${iconClass}`;
}
this.updateToggleMeta(theme);
if (persist) {
localStorage.setItem('theme', theme);
}
}
}

390
src/js/ui.js Normal file
View File

@@ -0,0 +1,390 @@
class Modal {
constructor(modalElement) {
this.modal = modalElement;
this.isOpen = false;
}
open() {
if (!this.modal) {
return Promise.reject(new Error('Modal element not found'));
}
this.modal.classList.remove('hidden');
document.body.classList.add('modal-open');
this.isOpen = true;
}
close() {
if (!this.modal) {
return;
}
this.modal.classList.add('hidden');
document.body.classList.remove('modal-open');
this.isOpen = false;
}
isModalOpen() {
return this.isOpen;
}
}
export class TemplateModal extends Modal {
constructor(modalElement, { validateTitle, validateText, getTemplates }) {
super(modalElement);
this.form = modalElement?.querySelector('.modal-form');
this.titleInput = modalElement?.querySelector('#modal-template-title');
this.ruInput = modalElement?.querySelector('#modal-template-ru');
this.enInput = modalElement?.querySelector('#modal-template-en');
this.cancelButton = modalElement?.querySelector('#modal-template-cancel');
this.validateTitle = validateTitle;
this.validateText = validateText;
this.getTemplates = getTemplates;
}
open() {
return new Promise((resolve, reject) => {
if (!this.modal || !this.form || !this.titleInput || !this.ruInput || !this.enInput) {
reject(new Error('Template modal is not available'));
return;
}
const cleanup = () => {
this.close();
this.form.removeEventListener('submit', handleSubmit);
if (this.cancelButton) {
this.cancelButton.removeEventListener('click', handleCancel);
}
this.modal.removeEventListener('click', handleBackdropClick);
document.removeEventListener('keydown', handleKeydown, true);
};
const handleCancel = () => {
cleanup();
reject(new Error('cancelled'));
};
const handleSubmit = (event) => {
event.preventDefault();
const templates = this.getTemplates();
const title = this.validateTitle(this.titleInput.value, templates);
if (!title) {
this.titleInput.focus();
return;
}
const ruText = this.validateText(this.ruInput.value);
if (ruText === null) {
this.ruInput.focus();
return;
}
const enText = this.validateText(this.enInput.value, true);
if (enText === null) {
this.enInput.focus();
return;
}
cleanup();
resolve({
title,
RUText: ruText,
ENText: enText
});
};
const handleBackdropClick = (event) => {
if (event.target === this.modal) {
handleCancel();
}
};
const handleKeydown = (event) => {
if (event.key === 'Escape') {
event.preventDefault();
handleCancel();
}
};
this.form.reset();
this.titleInput.value = '';
this.ruInput.value = '';
this.enInput.value = '';
super.open();
this.form.addEventListener('submit', handleSubmit);
if (this.cancelButton) {
this.cancelButton.addEventListener('click', handleCancel);
}
this.modal.addEventListener('click', handleBackdropClick);
document.addEventListener('keydown', handleKeydown, true);
requestAnimationFrame(() => {
this.titleInput.focus({ preventScroll: true });
});
});
}
}
export class ImportModal extends Modal {
constructor(modalElement) {
super(modalElement);
this.form = modalElement?.querySelector('#import-form');
this.jsonTextarea = modalElement?.querySelector('#import-json-input');
this.cancelButton = modalElement?.querySelector('#import-cancel');
this.applyButton = modalElement?.querySelector('#import-apply');
this.dropzone = modalElement?.querySelector('#import-dropzone');
this.fileTrigger = modalElement?.querySelector('#import-file-trigger');
this.fileInput = document.querySelector('.template-import-input');
this.filenameLabel = modalElement?.querySelector('#import-selected-filename');
this.fileCallback = null;
}
open() {
return new Promise((resolve, reject) => {
if (!this.modal || !this.form || !this.jsonTextarea) {
reject(new Error('Import modal unavailable'));
return;
}
let resolved = false;
const cleanup = () => {
this.close();
document.removeEventListener('keydown', handleKeydown, true);
this.modal.removeEventListener('click', handleBackdropClick);
this.form.removeEventListener('submit', handleSubmit);
if (this.cancelButton) {
this.cancelButton.removeEventListener('click', handleCancel);
}
if (this.dropzone) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
this.dropzone.removeEventListener(evt, handleDropzoneEvents);
});
this.dropzone.removeEventListener('keydown', handleDropzoneKeydown);
this.dropzone.classList.remove('drag-over');
}
this.fileCallback = null;
this.reset();
};
const finish = (value) => {
if (resolved) {
return;
}
resolved = true;
cleanup();
if (value === null) {
reject(new Error('cancelled'));
} else {
resolve(value);
}
};
const handleCancel = () => finish(null);
const handleBackdropClick = (event) => {
if (event.target === this.modal) {
finish(null);
}
};
const handleKeydown = (event) => {
if (event.key === 'Escape') {
event.preventDefault();
finish(null);
}
};
const handleSubmit = (event) => {
event.preventDefault();
const value = this.jsonTextarea.value.trim();
if (!value) {
alert('Вставьте JSON перед импортом или выберите файл.');
this.jsonTextarea.focus();
return;
}
finish(value);
};
const loadFileIntoTextarea = async (file) => {
if (!file) {
return;
}
try {
const text = await file.text();
this.jsonTextarea.value = text;
if (this.filenameLabel) {
this.filenameLabel.textContent = `Выбран файл: ${file.name}`;
}
} catch (error) {
console.error('Не удалось прочитать файл для импорта:', error);
alert('Не получилось прочитать файл. Попробуйте другой файл или вставьте содержимое вручную.');
}
};
const handleDropzoneEvents = async (event) => {
event.preventDefault();
if (!this.dropzone) {
return;
}
if (event.type === 'dragenter' || event.type === 'dragover') {
this.dropzone.classList.add('drag-over');
return;
}
if (event.type === 'dragleave') {
this.dropzone.classList.remove('drag-over');
return;
}
if (event.type === 'drop') {
this.dropzone.classList.remove('drag-over');
const file = event.dataTransfer?.files?.[0] || null;
if (file) {
await loadFileIntoTextarea(file);
}
}
};
const handleDropzoneKeydown = async (event) => {
if (!this.dropzone) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
if (this.fileTrigger) {
this.fileTrigger.click();
}
}
};
this.fileCallback = async (file) => {
await loadFileIntoTextarea(file);
};
if (this.fileTrigger && this.fileInput) {
this.fileTrigger.addEventListener('click', () => {
this.fileInput.value = '';
this.fileInput.click();
});
}
if (this.fileInput) {
this.fileInput.addEventListener('change', async () => {
if (this.fileCallback) {
const file = this.fileInput.files?.[0] || null;
await this.fileCallback(file);
}
});
}
if (this.dropzone) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
this.dropzone.addEventListener(evt, handleDropzoneEvents);
});
this.dropzone.addEventListener('keydown', handleDropzoneKeydown);
}
super.open();
document.addEventListener('keydown', handleKeydown, true);
this.modal.addEventListener('click', handleBackdropClick);
this.form.addEventListener('submit', handleSubmit);
if (this.cancelButton) {
this.cancelButton.addEventListener('click', handleCancel);
}
requestAnimationFrame(() => {
this.jsonTextarea.focus({ preventScroll: true });
});
});
}
reset() {
if (this.jsonTextarea) {
this.jsonTextarea.value = '';
}
if (this.filenameLabel) {
this.filenameLabel.textContent = '';
}
if (this.fileInput) {
this.fileInput.value = '';
}
}
}
export class CategoryManager {
constructor() {
this.controllers = [];
this.init();
}
init() {
document.querySelectorAll('.category').forEach(category => {
const header = category.querySelector('h3');
const content = category.querySelector('.category-content');
const icon = header?.querySelector('.toggle-icon');
if (!header || !content) {
return;
}
header.setAttribute('tabindex', '0');
header.setAttribute('role', 'button');
const categoryId = header.textContent.trim();
const setOpenState = (isOpen, persist = false) => {
if (isOpen) {
content.classList.add('show');
if (icon) {
icon.classList.add('fa-chevron-up');
icon.classList.remove('fa-chevron-down');
}
} else {
content.classList.remove('show');
if (icon) {
icon.classList.add('fa-chevron-down');
icon.classList.remove('fa-chevron-up');
}
}
if (persist) {
localStorage.setItem(categoryId, isOpen ? 'true' : 'false');
}
header.setAttribute('aria-expanded', String(isOpen));
};
const storedState = localStorage.getItem(categoryId);
const defaultOpen = category.dataset.category === 'custom' || category.dataset.category === 'favorites';
const isCategoryOpen = storedState === null ? defaultOpen : storedState === 'true';
setOpenState(isCategoryOpen);
const toggleCategory = (persist = true) => {
const nextState = !content.classList.contains('show');
setOpenState(nextState, persist);
};
header.addEventListener('click', () => {
toggleCategory(true);
});
header.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
toggleCategory(true);
}
});
this.controllers.push({
category,
content,
icon,
categoryId,
setOpenState: (isOpen, persist = false) => {
setOpenState(isOpen, persist);
}
});
});
}
getControllers() {
return this.controllers;
}
}

77
src/js/update-check.js Normal file
View File

@@ -0,0 +1,77 @@
import { REPO_BASE } from './constants.js';
/**
* Возвращает URL сырого manifest.json в репозитории.
* Gitea: .../raw/branch/{branch}/manifest.json
* GitLab: .../-/raw/{branch}/manifest.json
*/
function getManifestUrl(repoBase, branch = 'main') {
const base = repoBase.replace(/\/$/, '');
try {
const host = new URL(base).host.toLowerCase();
if (host.includes('gitlab')) {
return `${base}/-/raw/${branch}/manifest.json`;
}
return `${base}/raw/branch/${branch}/manifest.json`;
} catch {
return `${base}/raw/branch/${branch}/manifest.json`;
}
}
/**
* Сравнивает две версии в формате "X.Y.Z" (семантическое сравнение).
* @returns {number} -1 если current < latest, 0 если равны, 1 если current > latest
*/
function compareVersions(current, latest) {
if (!current || !latest) return 0;
const parse = (v) => v.replace(/^v/, '').split('.').map(Number);
const a = parse(current);
const b = parse(latest);
for (let i = 0; i < Math.max(a.length, b.length); i++) {
const x = a[i] ?? 0;
const y = b[i] ?? 0;
if (x < y) return -1;
if (x > y) return 1;
}
return 0;
}
/**
* Проверяет наличие новой версии расширения в репозитории.
* Требует разрешения на доступ к REPO_BASE (optional_host_permissions).
*
* @returns {Promise<{ hasUpdate: boolean, current: string, latest: string | null, downloadUrl: string, error?: string }>}
*/
export async function checkForUpdate() {
const current = typeof chrome !== 'undefined' && chrome.runtime?.getManifest
? chrome.runtime.getManifest().version
: '0.0.0';
const downloadUrl = REPO_BASE;
try {
let url = getManifestUrl(REPO_BASE, 'main');
let res = await fetch(url, { cache: 'no-store' });
if (res.status === 404) {
url = getManifestUrl(REPO_BASE, 'master');
res = await fetch(url, { cache: 'no-store' });
}
if (!res.ok) {
return { hasUpdate: false, current, latest: null, downloadUrl, error: `HTTP ${res.status}` };
}
const data = await res.json();
const latest = data?.version ? String(data.version).trim() : null;
if (!latest) {
return { hasUpdate: false, current, latest: null, downloadUrl, error: 'В репозитории не найден version в manifest' };
}
const hasUpdate = compareVersions(current, latest) < 0;
return { hasUpdate, current, latest, downloadUrl };
} catch (err) {
return {
hasUpdate: false,
current,
latest: null,
downloadUrl,
error: err?.message || 'Не удалось проверить обновления'
};
}
}

View File

@@ -18,14 +18,71 @@ 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;
gap: 12px;
flex-wrap: wrap;
} }
h1 { h1 {
padding: 6px; padding: 6px;
} }
.action-buttons {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.update-check-status {
padding: 6px 8px;
margin: 0 6px 6px;
font-size: 13px;
border-radius: 6px;
background: #e8f4e8;
color: #1a5c1a;
}
.update-check-status.update-check-error {
background: #fce8e8;
color: #8b2020;
}
.dark-theme .update-check-status {
background: rgba(40, 80, 40, 0.4);
color: #a8e0a8;
}
.dark-theme .update-check-status.update-check-error {
background: rgba(80, 40, 40, 0.4);
color: #e0a0a0;
}
.search-row {
padding: 0 6px 10px 6px;
}
.template-search {
width: 100%;
background-color: #f5f8ff;
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.template-search:focus-visible {
outline: none;
border-color: #7aa6e6;
box-shadow: 0 0 0 2px rgba(122, 166, 230, 0.2);
}
.dark-theme .template-search {
background-color: #2f2f2f;
color: #f5f5f5;
border-color: #555;
}
.dark-theme .template-search::placeholder {
color: rgba(245, 245, 245, 0.6);
}
h3 { h3 {
padding: 6px; padding: 6px;
font-family: Helvetica, Arial, sans-serif; /* Более минималистичный шрифт для категорий */ font-family: Helvetica, Arial, sans-serif; /* Более минималистичный шрифт для категорий */
@@ -81,6 +138,9 @@ span.template {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 12px;
height: fit-content; height: fit-content;
padding: 8px 7px; padding: 8px 7px;
border: 1px solid #8888c6; /* Цвет рамки совпадает с рамкой категории */ border: 1px solid #8888c6; /* Цвет рамки совпадает с рамкой категории */
@@ -102,8 +162,62 @@ span.template:hover {
color: #fff; /* Белый цвет текста в тёмной теме */ color: #fff; /* Белый цвет текста в тёмной теме */
} }
.template-title {
flex: 1 1 auto;
display: flex;
align-items: center;
gap: 6px;
word-break: break-word;
}
.template-favorite {
margin-right: 4px;
padding: 4px 8px;
background-color: transparent;
border: none;
color: #ffc107;
cursor: pointer;
font-size: 16px;
transition: transform 0.2s ease, color 0.2s ease;
}
.template-favorite:hover {
transform: scale(1.1);
color: #ff9800;
}
.template-favorite:focus-visible {
outline: 2px solid #ffc107;
outline-offset: 2px;
border-radius: 4px;
}
.dark-theme .template-favorite {
color: #ffc107;
}
.dark-theme .template-favorite:hover {
color: #ffb74d;
}
.bigFormochka {
flex: 1 1 auto;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
align-items: flex-start;
justify-content: flex-end;
}
.formochka { .formochka {
margin-left: 10px; flex: 1 1 220px;
min-width: 200px;
max-width: 320px;
display: flex;
flex-direction: column;
gap: 6px;
margin-left: 0;
} }
#template-title-input { #template-title-input {
@@ -113,11 +227,38 @@ span.template:hover {
span.buttons { span.buttons {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px;
}
span.template > span.buttons {
flex: 0 0 auto;
white-space: nowrap;
}
span.template > span.buttons:last-child {
justify-content: flex-end;
} }
span.buttonsLong { span.buttonsLong {
display: flex; display: flex;
justify-content: center; flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: 6px;
}
.buttonsShort {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 6px;
align-items: center;
justify-content: flex-start;
}
.buttonsShort .btn {
flex: 0 0 auto;
} }
span.template:hover { span.template:hover {
@@ -128,6 +269,178 @@ span.template:hover {
display: none; display: none;
} }
.filtered-out {
display: none !important;
}
.hotkey-badge {
display: inline-block;
margin-right: 6px;
padding: 2px 6px;
border-radius: 4px;
background-color: #8888c6;
color: #fff;
font-size: 0.6rem;
letter-spacing: 0.02em;
}
.dark-theme .hotkey-badge {
background-color: #555;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
z-index: 1500;
}
.modal-backdrop.hidden {
display: none;
}
.modal-card {
background: #ffffff;
color: #1c1c1c;
width: min(480px, 100%);
max-height: 95vh;
border-radius: 12px;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
}
.dark-theme .modal-card {
background: #2d2d2d;
color: #f1f1f1;
}
.modal-title {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
}
.modal-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.modal-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.modal-info {
font-size: 0.9rem;
color: #6c757d;
}
.modal-field span {
font-weight: 500;
}
.modal-field textarea,
.modal-field input {
width: 100%;
min-width: 0;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 8px;
}
.modal-backdrop .btn {
min-width: 92px;
}
.text-danger {
color: #c0392b;
}
.dark-theme .text-danger {
color: #e57373;
}
.import-dropzone {
border: 2px dashed #8aa1ff;
border-radius: 10px;
padding: 16px;
text-align: center;
background: rgba(138, 161, 255, 0.05);
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
cursor: pointer;
}
.import-dropzone:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(138, 161, 255, 0.35);
}
.import-dropzone.drag-over {
border-color: #4c6fff;
background: rgba(138, 161, 255, 0.18);
}
.import-dropzone-title {
margin: 0;
font-size: 1rem;
font-weight: 600;
}
.import-dropzone-subtitle {
margin: 6px 0 12px 0;
font-size: 0.85rem;
color: #6c757d;
}
.dark-theme .import-dropzone {
border-color: #6579d6;
background: rgba(101, 121, 214, 0.05);
}
.dark-theme .import-dropzone.drag-over {
border-color: #8aa1ff;
background: rgba(138, 161, 255, 0.25);
}
.import-dropzone-filename {
display: block;
margin-top: 10px;
font-size: 0.85rem;
color: #495057;
}
.dark-theme .import-dropzone-filename {
color: #d7dbdf;
}
.modal-field textarea#import-json-input {
font-family: 'Fira Code', 'JetBrains Mono', Consolas, 'Liberation Mono', monospace;
min-height: 180px;
resize: vertical;
}
.text-muted {
color: #6c757d;
}
.dark-theme .text-muted {
color: #9ca4ab;
}
.category { .category {
margin-bottom: 10px; margin-bottom: 10px;
border: solid #8888c6 1px; /* Тонкая рамка для категории */ border: solid #8888c6 1px; /* Тонкая рамка для категории */
@@ -144,6 +457,12 @@ span.template:hover {
cursor: pointer; /* Курсор при наведении на весь блок */ cursor: pointer; /* Курсор при наведении на весь блок */
width: 100%; /* Заголовок занимает всю ширину */ width: 100%; /* Заголовок занимает всю ширину */
box-sizing: border-box; /* Чтобы паддинги не влияли на размер */ box-sizing: border-box; /* Чтобы паддинги не влияли на размер */
outline: none;
}
.category h3:focus-visible {
box-shadow: 0 0 0 2px rgba(122, 166, 230, 0.35);
border-radius: 6px;
} }
.toggle-icon { .toggle-icon {
@@ -186,22 +505,37 @@ body.dark-theme {
} }
.theme-btn { .theme-btn {
position: absolute; display: inline-flex;
top: 10px; align-items: center;
left: 10px; justify-content: center;
background: transparent; width: 36px;
border: none; height: 36px;
cursor: pointer; border-radius: 50%;
font-size: 1.5rem; border: 1px solid #c5d9f7;
background: #fff;
color: #333; color: #333;
z-index: 1000; transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
transition: color 0.3s ease; }
.theme-btn:hover,
.theme-btn:focus-visible {
background: #e7f0fe;
border-color: #88aee5;
outline: none;
} }
.dark-theme .theme-btn { .dark-theme .theme-btn {
background: #2f2f2f;
border-color: #555;
color: #fff; color: #fff;
} }
.dark-theme .theme-btn:hover,
.dark-theme .theme-btn:focus-visible {
background: #3e3e3e;
border-color: #777;
}
/* Кнопка добавления шаблона */ /* Кнопка добавления шаблона */
.add-template-btn { .add-template-btn {
position: absolute; position: absolute;
@@ -254,14 +588,23 @@ body.dark-theme {
/* Стили для input и textarea */ /* Стили для input и textarea */
input, textarea { input, textarea {
width: 100%; width: 100%;
height: 40px; /* Сделаем высоту фиксированной */ max-width: 100%;
min-width: 0;
padding: 8px; padding: 8px;
box-sizing: border-box; box-sizing: border-box;
margin-top: 4px; margin-top: 4px;
} }
/* Убираем возможность изменения размеров textarea */
textarea { textarea {
resize: none; resize: vertical;
min-height: 64px;
}
body.modal-open {
overflow: hidden;
}
.dark-theme body.modal-open {
overflow: hidden;
} }

View File

@@ -11,16 +11,42 @@
</head> </head>
<body> <body>
<button id="theme-toggle" class="theme-btn">
<i id="theme-icon" class="fa-moon fa-solid"></i>
</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="action-buttons">
<button id="theme-toggle" class="theme-btn" type="button" title="Переключить тему">
<i id="theme-icon" class="fa-solid fa-moon"></i>
</button>
<button class="template-add btn btn-primary" type="button" title="Добавить шаблон"><i class="fa-solid fa-plus"></i></button>
<button class="template-export btn btn-outline-secondary" type="button" title="Экспортировать шаблоны"><i class="fa-solid fa-file-arrow-down"></i></button>
<button class="template-import btn btn-outline-secondary" type="button" title="Импортировать шаблоны"><i class="fa-solid fa-file-arrow-up"></i></button>
<button id="update-check-btn" class="btn btn-outline-secondary" type="button" title="Проверить обновления в репозитории"><i class="fa-solid fa-arrows-rotate"></i></button>
<input class="template-import-input" type="file" accept="application/json" hidden>
</div>
</div>
<div id="update-check-status" class="update-check-status hidden" aria-live="polite"></div>
<div class="search-row">
<input type="search" class="template-search form-control form-control-sm" placeholder="Поиск шаблонов">
</div> </div>
<div class="templates" id="template-list"> <div class="templates" id="template-list">
<div class="category" data-category="favorites">
<h3 class="font-monospace">
Избранное
<i class="fa fa-chevron-down toggle-icon"></i>
</h3>
<div class="category-content" id="favorites-template-container">
</div>
</div>
<div class="category" data-category="custom">
<h3 class="font-monospace">
Мои шаблоны
<i class="fa fa-chevron-down toggle-icon"></i>
</h3>
<div class="category-content" id="custom-template-container">
</div>
</div>
<div class="category"> <div class="category">
<h3 class="font-monospace"> <h3 class="font-monospace">
Промежуточные ответы Промежуточные ответы
@@ -460,9 +486,54 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal-backdrop hidden" id="template-modal" role="dialog" aria-modal="true">
<div class="modal-card">
<h2 class="modal-title">Новый шаблон</h2>
<form class="modal-form" novalidate>
<label class="modal-field">
<span>Название <span class="text-danger">*</span></span>
<input type="text" id="modal-template-title" class="form-control" required minlength="3" autocomplete="off">
</label>
<label class="modal-field">
<span>Текст (RU) <span class="text-danger">*</span></span>
<textarea id="modal-template-ru" class="form-control" rows="4" required minlength="5"></textarea>
</label>
<label class="modal-field">
<span>Текст (EN)</span>
<textarea id="modal-template-en" class="form-control" rows="4" minlength="5"></textarea>
</label>
<div class="modal-actions">
<button type="button" class="btn btn-outline-secondary" id="modal-template-cancel">Отмена</button>
<button type="submit" class="btn btn-primary" id="modal-template-save">Сохранить</button>
</div> </div>
</1--div> </form>
<script src="popup.js"></script> </div>
</div>
<div class="modal-backdrop hidden" id="import-modal" role="dialog" aria-modal="true">
<div class="modal-card">
<h2 class="modal-title">Импорт шаблонов</h2>
<form class="modal-form" id="import-form" novalidate>
<div class="modal-info">Выберите JSON-файл или вставьте данные вручную.</div>
<div class="import-dropzone" id="import-dropzone" tabindex="0">
<p class="import-dropzone-title">Перетащите файл сюда</p>
<p class="import-dropzone-subtitle">или</p>
<button type="button" class="btn btn-outline-primary" id="import-file-trigger">Выбрать файл</button>
<span class="import-dropzone-filename" id="import-selected-filename"></span>
</div>
<label class="modal-field">
<span>JSON содержимое <span class="text-muted">(будет распознано из файла автоматически)</span></span>
<textarea id="import-json-input" class="form-control" rows="8" spellcheck="false" placeholder="{ &quot;templates&quot;: [ ... ] }"></textarea>
</label>
<div class="modal-actions">
<button type="button" class="btn btn-outline-secondary" id="import-cancel">Отмена</button>
<button type="submit" class="btn btn-primary" id="import-apply">Импортировать</button>
</div>
</form>
</div>
</div>
</div>
<script type="module" src="popup.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,297 +1,334 @@
document.addEventListener("DOMContentLoaded", function () { import { loadClipboardTexts, initializeClipboardButtons, copyToClipboard } from './js/clipboard.js';
// Объект с текстами для каждого элемента import { TemplateStorage } from './js/storage.js';
const clipboardTexts = { import {
"inWorkRu": "Коллеги, здравствуйте.\n\nПриняли Ваш запрос в работу. Сообщим по мере поступления информации.", generateTemplateId,
"inWorkEn": "Dear customer,\n\nWe are working on your request.", validateTitle,
"dialogueRu": "Коллеги, здравствуйте.\n\nУведомляем вас о том, что мы начали диалог с оператором по данному запросу. Мы ожидаем ответа и будем держать вас в курсе любых изменений.", validateText,
"dialogueEn": "Dear colleagues.\n\nWe would like to inform you that we have started a dialogue with the operator on this request. We are awaiting a response and will keep you informed of any changes.", createTemplateElement,
"nonDelRu": "Коллеги, здравствуйте.\n\nПросьба уточнить причину недоставки следующих сообщений клиенту:", downloadJsonFile,
"nonDelEn": "Dear colleagues,\n\nPlease check why the following messages were not delivered:", importTemplatesFromText
"fixItRu": "Коллеги,\n\nМы не фиксируем на нашей платформе ваших СМС-сообщений, просьба предоставить ответ от нашей платформы на ваш запрос отправки. Так же просьба предоставить логи, подтверждающие отправку на нашу платформу, данная информация необходима для дальнейшего анализа вашего запроса.", } from './js/templates.js';
"fixItEn": "Dear colleagues,\n\nWe do not record your SMS messages on our platform, please provide a response from our platform to your request to send. Please also provide logs confirming sending to our platform, this information is necessary for further analysis of your request.", import { TemplateModal, ImportModal, CategoryManager } from './js/ui.js';
"fakeRu": "Коллеги, здравствуйте.\n\nПросьба уточнить корректность статусов доставки следующих сообщений клиенту. Абонент не получал СМС.", import { HotkeyManager } from './js/hotkeys.js';
"fakeEn": "Dear colleagues,\n\nPlease clarify the correctness of the delivery statuses of the following messages to the client. The subscriber did not receive an SMS.", import { SearchManager } from './js/search.js';
"weworkRu": "Коллеги, здравствуйте.\n\nУведомляем вас о том, что работы по вашему запросу продолжаются. Как только появится информация, мы вам сообщим.\n\nБлагодарим за понимание и приносим извинения за доставленные неудобства.", import { ThemeManager } from './js/theme.js';
"weworkEn": "Dear Colleagues.\n\nWe notify you that work on your request is ongoing. As soon as information becomes available, we will inform you.\n\nThank you for your understanding and we apologize for the inconvenience caused.", import { checkForUpdate } from './js/update-check.js';
"wework2Ru": "Коллеги, здравствуйте.\n\nЖдем ответа от оператора. Сообщим, как только появится соответствующая информация. Спасибо!",
"wework2En": "Dear Colleagues.\n\nWe are waiting for a response from the operator. We will inform you as soon as we have an update. Thank you!", document.addEventListener("DOMContentLoaded", async function () {
"wework3Ru": "Здравствуйте!\n\nОтправили повторный запрос оператору по данному поводу.\n\nСпасибо!", const templateStorage = new TemplateStorage();
"wework3En": "Hello!\n\nWe have sent an additional request to the operator.\n\nThank you!", await templateStorage.load();
"theyworkRu": "Здравствуйте, коллеги.\n\nПоявилась ли информация по данному вопросу?\n\nСпасибо.",
"theyworkEn": "Dear colleagues,\n\nIs there any information on this request?\n\nThank you.", const clipboardTexts = await loadClipboardTexts();
"theywork2Ru": "Добрый день!\n\nПросим вернуться с ответом.\n\nБольшое спасибо!", let favorites = await templateStorage.getFavorites();
"theywork2En": "Hello Team!\n\nPlease let us know if there are any updates.\n\nThank you!",
"theywork3Ru": "Здравствуйте!\n\nПодскажите, пожалуйста, появились ли новости.\n\nСпасибо!", const toggleFavoriteCallback = async (templateId, isFavorite) => {
"theywork3En": "Hello!\n\nPlease kindly advise on issue status.\n\nThank you.", favorites = await templateStorage.toggleFavorite(templateId, isFavorite);
"delivedRu": "Коллеги,\n\nСообщения успешно прошли через платформу и были отправлены оператору. По информации от оператора, сообщения были своевременно доставлены. В случае, если абонент утверждает, что сообщения не были получены, просьба проверить исправность работы ТА. Возможные рекомендации: перезагрузка телефона, очистка памяти от устаревших сообщений, проверка спам-фильтров и черных списков, обновление ПО.\n\nПри сохранении проблемы, просьба запросить у абонента детализацию. Данная информация потребуется для дальнейшего взаимодействия с оператором.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes", updateFavoritesDisplay();
"delivedEn": "Colleagues, according to the information from the operator, messages were delivered in a timely manner. If the subscriber claims that the messages have not been received, please check that device is working correctly. Possible recommendations: reboot the phone, clear message memory, check spam filters and blacklists, update the software.\n\nIf the problem still persists, please ask the subscriber for details. This information is necessary to continue interaction with the operator.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"problphRu": "Коллеги,\n\nСообщения успешно прошли через платформу и были отправлены оператору. По информации от оператора, сообщения не были доставлены абоненту по причине сбоя ТА. Возможные рекомендации для абонента: перезагрузка телефона, очистка памяти от устаревших сообщений, проверка спам-фильтров и черных списков, обновление ПО.\n\nПросьба уведомить нас в случае, если ситуация повторится.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"problphEn": "Colleagues, according to the information from the operator, messages were not delivered due to subscribers device failure. Possible recommendations: reboot the phone, clear message memory, check spam filters and blacklists, update the software.\n\nPlease let us know if the problem persist.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"notservRu": "Здравствуйте, коллеги.\n\nСообщения успешно прошли через платформу и были отправлены оператору. По информации от оператора, сообщения абоненту не были доставлены, т.к. номер телефона не существует или не обслуживается.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"notservEn": "Colleagues\n\nThe message successfully passed through the platform and was sent to the operator. According to the information from the operator, the messages were not delivered to the subscriber because the phone number does not exist or is not serviced.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"insentRu": "Коллеги,\n\nСообщения успешно прошли через платформу и были отправлены оператору. Сообщения находятся в статусе Отправлено, SMS-центр оператора продолжает осуществлять попытки их доставки.\n\nНаиболее вероятной причиной задержки в доставке может являться нахождение абонента в зоне неуверенного приема сети, также возможен технический сбой в работе телефона абонента.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"insentEn": "Colleagues,\n\nThe messages are in Sent status, operator's SMS center is continuing delivery attempts. Most likely, delivery delays are caused by subscriber being in the area of uncertain network reception, or a technical failure in the operation of the subscriber's phone is also possible.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"dubleRu": "Коллеги,\n\nМониторингом были зафиксированы дубликаты сообщений, в связи с чем сработала блокировка. Если в короткий промежуток времени на площадку поступает два одинаковых сообщения, повторное блокируется как дубликат.\n\nЕсли отправка дубликатов была запланирована, либо номер абонента используется для тестирования подключения, просьба сообщить, мы добавим абонента в белый список.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"dubleEn": "Colleagues, duplicates were blocked by our monitoring system. If two identical messages are received within a short period of time, the second one is blocked as a duplicate.\n\nIf sending duplicates was planned, or the subscriber's number is used for testing, please let us know and we will whitelist the subscriber.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"unvelbRu": "Коллеги,\n\nСообщения успешно прошли через платформу и были отправлены оператору. Сообщения абоненту не были доставлены т.к. ТА абонента был недоступен - выключен или находился в зоне неуверенного приема сигнала.\n\nРекомендуем абоненту перезагрузить телефон, проверить доступность сети оператора, очистить память телефона от устаревших сообщений.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"unvelbEn": "Colleagues,\n\nThe messages were not delivered to the subscriber because the subscriber's phone was unavailable - turned off or was in the zone of uncertain signal reception.\n\nWe recommend that the subscriber restart the phone, check the availability of the operator's network, and clear the phone's memory of outdated messages.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"delayRu": "Коллеги, здравствуйте.\n\nПросьба уточнить текущий статус сообщений и причины задержек:",
"delayEn": "Dear Colleagues.\n\nPlease clarify current messages status and the reasons for the delays:",
"conbreRu": "Коллеги, здравствуйте.\n\nНаблюдаем разрыв соединения. Просьба уточнить, ситуация штатная?",
"conbreEn": "Dear colleagues.\n\nWe observe a connection break. Please clarify whether its a standard situation.",
"corroutRu": "Коллеги,\n\nПо информации от оператора, для абонента были внесены корректировки в таблицы маршрутизации.\n\nПросьба проверить и сообщить в случае повторения ситуации.",
"corroutEn": "Dear colleagues,\n\nThe adjustments have been made in routing settings.\nAs our logs are now showing positive delivery results, we would like to ask you to perform a fresh check on your side and share the results in case if further assistance is required.",
"tehtroubRu": "Коллеги,\n\nПо информации от оператора, сообщения не были доставлены абоненту из-за временных технических неполадок на стороне оператора. На текущий момент доставка сообщений осуществляется в штатном режиме.\n\nПросьба проверить и сообщить в случае повторения ситуации.",
"tehtroubEn": "Dear colleagues,\n\nAccording to information we got from the operator, the messages were not delivered to the subscriber due to temporary technical issues on the operator's side. At the moment, messages are being delivered in the regular mode.",
"senderRu": "Коллеги,\n\nСообщения не были доставлены, так как для имени отправителя ######## требуется регистрация на оператора ###Имя бренда###.\n\nПросьба подать имя на регистрацию в ЛК или обратиться по данному вопросу к Вашему менеджеру.",
"senderEn": "Colleagues, the below reported traffic failed to be delivered due to an unregistered Sender ID ########. Alphanumeric registration is required towards ###COUNRTY OPERATOR (MCCMNC)###, and delivery of messages with unregistered Sender IDs is on the best effort basis. To register, please contact your Account Manager.",
"notResponseRU": "Здравствуйте!\n\nК сожалению, мы не получили от Вас ответа в течение 20 дней, поэтому запрос будет автоматически закрыт. Вы можете ответить на это письмо, если проблема не решена и требуется поддержка.",
"notResponseEn": "Hello!\n\nUnfortunately, we have not received a response from you within 20 days, so the request will be automatically closed. You can reply to this email if the problem has not been resolved and support is required.",
"fwd2treadRU": "Коллеги, здравствуйте.\n\nПередали вопрос ответственной команде. Сообщим по мере поступления информации.",
"fwd2treadEn": "Dear colleagues,\n\nWe have escalated the issue to the responsible team.\nWe will keep you informed as we receive any updates.",
"daoffice1RU": "Добрый день!\n\nДля регистрации в приложении \"КингДом\" необходимо внести Ваш номер телефона в Axapta.\n\nПо данному вопросу, пожалуйста, обратитесь к директору своего ресторана или на электронную почту: kingdom@burgerking.ru\n\nЧерез три часа после внесения номера, Вы сможете войти в сеть по номеру телефона, используя приложение \"КингДом\".",
"officeRU": "Добрый день!\n\nМы проверили Вашу учетную запись - все данные корректны.\nПроверочный СМС-код успешно отправлен на Ваш мобильный телефон.\n\nЕсли у Вас устройство на базе Android, то пожалуйста, переустановите приложение \"КингДом\" (удалить\\скачать заново) по следующей ссылке ниже, чтобы исключить отсутствие свежих обновлений: https://play.google.com/store/apps/details?id=ru.kingdom\n\nЕсли у Вас устройство на базе IOS, то пожалуйста, не удаляйте приложение \"КингДом\", так как оно было удалено из магазина приложений AppStore в связи с изменениями политики Apple Inc.\nВ настоящий момент, Вы можете пользоваться веб-версией приложения \"КингДом\" через браузер или уже установленной версий приложения.\nКоманда разработчика уже ведёт работу по восстановлению приложения.\nМы сообщим как только приложение будет доступно для скачивания в AppStore.\n\nПриложения для смартфонов на базе Android доступно для скачивания без изменений.\n\nЕсли данные действия не помогут, то, пожалуйста, опишите подробнее, на каком моменте у Вас не получается зайти в систему (при наличии ошибки приложите скриншот).",
"blockMsisdnRu": "Коллеги,\n\nДанный номер находится в глобальном ЧС.\n\nЭто означает, что клиент жаловался на получение СМС и отказался от получения всех СМС, либо были жалобы от данного абонента в УФАС.\n\nИз глобального ЧС, к сожалению, мы не можем разблокировать по Вашей просьбе, только в случае, если абонент напишет сам о просьбе разблокировать, с пометкой о согласии получения всех сообщений от оператора.\n\nЗаявление пишется в свободной форме, скан необходимо прислать на почту вашему менеджеру. Пример:\n\nКому: ООО «АйДиджитал»\nГенеральному директору: Баранову А.В.\nПрошу убрать мой номер ____________ из черного списка. Понимаю и соглашаюсь на возможное получение рассылок от других компаний, которые осуществляют рассылки через платформу АйДиджитал.\n\nПодпись и дата.",
"commonSenderRu": "Коллеги, сообщения были заблокированы согласно правилам отправки для каналов с Общим именем. А именно -\n----------------------------\n в тексте сообщения содержится упоминания международного бренда (рассылки в интересах международных брендов).\n на канале запрещена отправка сообщений, если в качестве сендера используется международный сендер\n на канале запрещена отправка сообщений на латинице\n на канале запрещена отправка сообщений с кодами в тексте которых нет упоминания сервиса от которого идет отправка\n на канале запрещена отправка сообщений в тексте которых содержится ссылка (ссылкой считается - наличие http в URL)\n на канале запрещена отправка сообщений в тексте сообщений которых указаны только цифры\n на канале запрещена отправка сообщений с менее 13 символов в тексте",
"noResponse": "Здравствуйте!\n\nК сожалению, мы не получили от Вас ответа в течение 5 рабочих дней, поэтому запрос будет автоматически закрыт.\n\nВы можете ответить на это письмо, если проблема не решена и требуется дополнительная помощь.",
"conductedTesting": "Коллеги,\n\nМы провели тестирование данного направления, используя живые номера. Все сообщения были доставлены на устройства правильно и без задержек.\n\nПросьба провести повторное тестирование со своей стороны и сообщить в случае повторения ситуации.",
"HLRRu": "Коллеги, здравствуйте.\\nПросьба уточнить причину возврата некорректных HLR статусов на указанные номера:",
"HLREn": "Dear colleagues,\n\nPlease clarify the correctness of HLR statuses for the following subscriber numbers:",
"managerTemplate": "Страна: \nОператор: \nmccMnc: \nСендер: \nКлиент: \nSource node: \nDestination node: ",
"suspiciousActivity": "Здравствуйте, коллеги!\n\nНашей системой обнаружена аномальная активность по направлению \"\".\n\nПросим, пожалуйста, проверить и сообщить является ли данная ситуация штатной.\n\nГрафик с общим количеством трафика по направлению:\n\n\nКоличество трафика по часам:\n\n\nКоличество сообщений по номерам:\n\n\nПеребор по номерам:\n\n\nПожалуйста, дайте нам знать, если у вас возникли вопросы или требуется дополнительная информация.",
"trafficBlocking": "Здравствуйте, коллеги!\n\nНашей системой обнаружена аномальная активность по направлению \"\".\n\nУведомляем вас о том, что трафик по данному направлению был ЗАБЛОКИРОВАН. Просим, пожалуйста, проверить и сообщить является ли данная ситуация штатной.\n\nГрафик с общим количеством трафика по направлению:\n\n\nКоличество трафика по часам:\n\n\nКоличество сообщений по номерам:\n\n\nПеребор по номерам:\n\n\nПожалуйста, дайте нам знать, если у вас возникли вопросы или требуется дополнительная информация.",
"nonDelPushRu": "Коллеги,\n\nПуш-сообщения не были доставлены по причине истечения TTL. ТА абонента не вернул статус получения пуш-сообщения в установленное время, поэтому сообщению присвоен статус UNDELIVERED. \n\nВозможными причинами недоставки могут быть: отсутствие подключения к интернету, режим энергосбережения или \"тихий\" режим, на устройстве включен \"Режим полета\", отсутствие прав у приложения на отправку уведомлений.",
"nonDelPushEng": "Colleagues,\n\nPush messages were not delivered due to TTL expiration. The subscriber's device did not return the status of receiving the push message at the set time, so the message was assigned the status UNDELIVERED. \n\nPossible reasons for non-delivery may be: lack of Internet connection, power saving mode or \"silent\" mode, Airplane Mode is enabled on the device, and the app does not have rights to send notifications.",
"riveGaucheStaff":"Здравствуйте, коллеги!\n\nНашей системой обнаружена аномальная активность по направлению \"УКАЗАТЬ БРЕНД И СТРАНУ\".\n\nПросим, пожалуйста, проверить и сообщить является ли данная ситуация штатной.\n\nЦРТ -\n\nГрафик с общим количеством трафика по направлению:\n\n\nКоличество трафика по часам:\n\nКоличество сообщений по номерам:\n\nПеребор по номерам:\n\nПожалуйста, дайте нам знать, если у вас возникли вопросы или требуется дополнительная информация.",
"riveGaucheBlock":"Здравствуйте, коллеги!\n\nНашей системой обнаружена аномальная активность по направлению \"УКАЗАТЬ БРЕНД И СТРАНУ\".\n\nУведомляем вас о том, что трафик по данному направлению был ЗАБЛОКИРОВАН. Просим, пожалуйста, проверить и сообщить является ли данная ситуация штатной.\n\nЦРТ - \n\nГрафик с общим количеством трафика по направлению:\n\n\nКоличество трафика по часам:\n\n\nКоличество сообщений по номерам:\n\n\nПеребор по номерам:\n\nПожалуйста, дайте нам знать, если у вас возникли вопросы или требуется дополнительная информация.",
"ahtungDefault":"Направление: \nПроблема: 100% недоставка | задержки\nЗапрос оператору: \nКого затронуло: \n\nАхтунг: направили\nВремя начала: \nОтветственный за решение инцидента: ",
"504Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. сервис коротких сообщений не предоставляется абоненту.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"504En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the short message service is not provided to the subscriber.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"505Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения абоненту не были доставлены, т.к. у абонента включен запрет на прием смс сообщений или абонент заблокирован оператором (возможно, в связи с отрицательным балансом).\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"505En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber has a ban on receiving messages or the subscriber was blocked by the operator (possibly due to a negative balance).\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"506Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. отправка смс сообщений на указанный номер недоступна. Причиной ограничения может быть блокировка входящих смс сообщений на телефоне, либо полное отключение услуги.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"506En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: sending SMS messages to the specified number is unacceptable. The reasons for the restriction may be blocking incoming SMS messages on the phone, or completely disabling the service.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"508Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения абоненту не были доставлены т.к. ТА абонента был недоступен - выключен или находился в зоне неуверенного приема сигнала.\nРекомендуем абоненту перезагрузить телефон, проверить доступность сети оператора, очистить память телефона от устаревших сообщений.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"508En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's phone was unavailable - turned off or was in the zone of uncertain signal reception.\nWe recommend that the subscriber restart the phone, check the availability of the operator's network, and clear the phone's memory of outdated messages.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"509Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. абонент находится в роуминге. У оператора, который доставляет сообщение, нет соглашения на роуминг с роуминг-оператором.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"509En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber is roaming, but the operator delivering the message does not have a roaming agreement with the roaming operator.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"510Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. на момент отправки сообщений абонентом была произведена замена SIM-карты. В течение 24-х часов с момента замены SIM-карты могут наблюдаться трудности в доставке смс сообщений.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"510En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: at the time of sending the messages, the subscriber had replaced the SIM card. The operator rejects SMS messages within 24 hours from the moment the SIM card is replaced.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"511Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. очередь сообщений для абонента со стороны оператора переполнена, в связи с чем оператор отклонил сообщение.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"511En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the message was rejected by the operator due to a full message queue on the recipient's side.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"515Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. на попытки доставки сообщений до ТА абонента возвращается ошибка, которая свидетельствует о некорректной работе ТА в сети или нехватке памяти на ТА абонента.\n\nАбоненту рекомендуется:\n1. Очистить память аппарата от устаревших сообщений\n2. Проверить свой аппарат на наличие сбоев, перезагрузить аппарат, проверить черные списки и спам-фильтры аппарата\n3. Удалить стороннее ПО для работы с sms, установленное на аппарате\n4. При необходимости провести обновление системного ПО, или сброс настроек аппарата к заводским.\n5. Также абоненту рекомендуется протестировать прием данных сообщений, переставив sim-карту в другой, заведомо исправный аппарат.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"515En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: when attempts are made to deliver messages to the subscriber, an error is returned, which indicates that subscriber's phone is not working correctly on the network or that there is a lack of memory on the subscriber.\n\nWe recommend the subscriber to:\n1. Clear the handset memory of out-of-date messages;\n2. Check the handset for failures, reboot the device, check blacklists and spam filters\n3. Remove third-party software for working with sms installed on the device;\n4. If necessary, update the system software, or reset the device to original settings;\n5. Test the reception by moving the SIM card to another device known to be working.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"517Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. на попытки доставки сообщений до ТА абонента возвращается ошибка, которая свидетельствует о некорректной работе ТА в сети или нехватке памяти на ТА абонента.\n\nАбоненту рекомендуется:\n1. Очистить память аппарата от устаревших сообщений\n2. Проверить свой аппарат на наличие сбоев, перезагрузить аппарат, проверить черные списки и спам-фильтры аппарата\n3. Удалить стороннее ПО для работы с sms, установленное на аппарате\n4. При необходимости провести обновление системного ПО, или сброс настроек аппарата к заводским.\n5. Также абоненту рекомендуется протестировать прием данных сообщений, переставив sim-карту в другой, заведомо исправный аппарат.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"517En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: when attempts are made to deliver messages to the subscriber, an error is returned, which indicates that subscriber's phone is not working correctly on the network or that there is a lack of memory on the subscriber.\n\nWe recommend the subscriber to:\n1. Clear the handset memory of out-of-date messages;\n2. Check the handset for failures, reboot the device, check blacklists and spam filters\n3. Remove third-party software for working with sms installed on the device;\n4. If necessary, update the system software, or reset the device to original settings;\n5. Test the reception by moving the SIM card to another device known to be working.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"523Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. ТА абонента занят операцией, препятствующей получению короткого сообщения.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"523En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's phone is busy with an operation that prevents the receipt of a short message.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"526Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. ТА абонента (IMEI) не определен или не разрешен для использования в сети оператора.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"526En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's device (IMEI) is not recognized or is not authorized for use on the operator's network.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"527Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. ТА абонента был недоступен - выключен или находился в зоне неуверенного приема сигнала.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"527En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's phone was unavailable - turned off or was in an area of uncertain signal reception.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"529MTSRu": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. у получателя активирована услуга «Запрет на SMS информирование».\n\nДанная опция блокирует массовые рассылки - зачастую это рекламные рассылки, опросы или уведомления от магазинов. Однако в зону блокировки могут попасть и уведомления от банков, сервисов авторизации или бронирования. Чтобы начать вновь получать сообщения, получателю необходимо обратиться к своему оператору связи и отключить услугу «Запрет на SMS информирование». После этого доставка SMS будет восстановлена.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"529MTSEn": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the recipient has activated the \"Ban on SMS notification\" service.\n\nThis option blocks mass mailings, which are often promotional mailings, surveys, or notifications from stores. However, notifications from banks, authorization services, or booking services may also be blocked. To start receiving messages again, the recipient needs to contact their telecom operator and disable the \"Ban on SMS notification\" service. After that, SMS delivery will be restored.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"529MegaRu": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. был указан некорректный номер абонента, либо нет правил маршрутизации для данного абонента или ошибка при отправке на блокированные номера (по закону Кудрявцева).\n\nУточняем что с 1 июня вступили в силу поправки в Федеральный закон \"О связи\", касающиеся в основном регистрации корпоративных SIM-карт и противодействия незаконной реализации SIM-карт.\nТеперь юридические лица и индивидуальные предприниматели обязаны предоставлять операторам связи и на \"Госуслугах\" данные о сотрудниках, использующих корпоративные SIM-карты, а также о номерах в устройствах M2M/IoT. Возможные рекомендации: проверка своих паспортных данных на госуслугах, привязка номера телефона к действующему аккаунту на госуслугах.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"529MegaEn": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: an incorrect subscriber number was specified, or there are no routing rules for this subscriber, or an error was made when sending to blocked numbers (according to Kudryavtsev's law).\n\nWe would like to clarify that on June 1, amendments to the Federal Law \"On Communications\" came into force, mainly concerning the registration of corporate SIM cards and countering the illegal sale of SIM cards.\nNow, legal entities and individual entrepreneurs are required to provide telecom operators and Public Services with information about employees using corporate SIM cards, as well as numbers in M2M/IoT devices. Possible recommendations: checking your passport data on public services, linking your phone number to your current account on public services.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"532Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. были зафиксированы дубликаты сообщений, в связи с чем сработала блокировка.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"532En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: duplicate messages were recorded, and therefore the lock was triggered.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"561Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. номер абонента некорректен или принадлежит стационарному телефону.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"561En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's number is incorrect.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"575Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. в целях защиты абонента от мошеннических и противоправных действий на номере на текущий момент установлены ограничения на приём входящих смс. Доставка смс будет возобновлена после снятия ограничений.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"575En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: In order to protect the subscriber from fraudulent and illegal actions, the number currently has restrictions on receiving incoming SMS messages. SMS delivery will be resumed after the restrictions are lifted.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes"
}; };
// Функция для обработки кликов const updateFavoritesForPredefined = async () => {
function handleClipboardClick(buttonId) { favorites = await templateStorage.getFavorites();
if (clipboardTexts[buttonId]) { initializeClipboardButtons(clipboardTexts, favorites, toggleFavoriteCallback);
navigator.clipboard.writeText(clipboardTexts[buttonId]); };
}
}
// Привязываем обработчик событий к кнопкам await updateFavoritesForPredefined();
Object.keys(clipboardTexts).forEach(buttonId => {
const button = document.getElementById(buttonId); const customTemplatesContainer = document.getElementById('custom-template-container');
if (button) { const favoritesContainer = document.getElementById('favorites-template-container');
button.addEventListener("click", () => handleClipboardClick(buttonId)); const customCategory = document.querySelector('[data-category="custom"]');
const templatesRoot = document.querySelector('.templates');
const updateCustomCategoryVisibility = () => {
const hasTemplates = templateStorage.getAll().length > 0;
if (customCategory) {
customCategory.style.display = hasTemplates ? '' : 'none';
} }
}); };
//---------------------------------------------------Создание новых шаблонов локально---------------------------------------------------------//
const templatesEl = document.querySelector('.templates');
const addBtn = document.querySelector('.template-add'); const addBtn = document.querySelector('.template-add');
const exportBtn = document.querySelector('.template-export');
const importBtn = document.querySelector('.template-import');
const categoryManager = new CategoryManager();
const hotkeyManager = new HotkeyManager();
const searchManager = new SearchManager(categoryManager, hotkeyManager);
const themeManager = new ThemeManager();
const templateModal = new TemplateModal(
document.getElementById('template-modal'),
{
validateTitle: (title, ignoreId) => validateTitle(title, templateStorage.getAll(), ignoreId),
validateText,
getTemplates: () => templateStorage.getAll()
}
);
const importModal = new ImportModal(document.getElementById('import-modal'));
const templateCallbacks = {
onUpdate: async (id, key, value) => {
await templateStorage.update(id, key, value);
},
onDelete: async (id) => {
await templateStorage.remove(id);
favorites = favorites.filter(favId => favId !== id);
await templateStorage.saveFavorites(favorites);
updateFavoritesDisplay();
updateCustomCategoryVisibility();
},
onHotkeyUpdate: () => {
hotkeyManager.updateAssignments();
},
onToggleFavorite: toggleFavoriteCallback,
getTemplates: () => templateStorage.getAll()
};
const renderTemplate = (template, container = null) => {
const isFavorite = favorites.includes(template.id);
const element = createTemplateElement({ ...template, favorite: isFavorite }, templateCallbacks);
const targetContainer = container || customTemplatesContainer || templatesRoot;
targetContainer.appendChild(element);
hotkeyManager.updateAssignments();
};
const normalizePredefinedId = (id) => {
if (!id) return id;
return id.replace(/Ru$|En$|Eng$/, '');
};
const createPredefinedTemplateElement = (normalizedId, clipboardTexts) => {
const ruButtonId = normalizedId + 'Ru';
const enButtonId = normalizedId + 'En';
const ruButton = document.getElementById(ruButtonId) || document.getElementById(normalizedId + 'Eng');
const enButton = document.getElementById(enButtonId) || document.getElementById(normalizedId + 'Eng');
const button = ruButton || enButton;
if (!button) return null;
const originalTemplate = button.closest('.template');
if (!originalTemplate) return null;
const titleEl = originalTemplate.querySelector('.template-title');
let title = normalizedId;
if (titleEl) {
const hotkeyBadge = titleEl.querySelector('.hotkey-badge');
if (hotkeyBadge) {
title = titleEl.cloneNode(true);
title.querySelector('.hotkey-badge')?.remove();
title = title.textContent.trim();
} else {
title = titleEl.textContent.trim();
}
}
const ruText = clipboardTexts[ruButtonId] || '';
const enText = clipboardTexts[enButtonId] || clipboardTexts[normalizedId + 'Eng'] || '';
function createTemplate(title, RUText, ENText) {
const templateId = Date.now();
const templateEl = document.createElement('span'); const templateEl = document.createElement('span');
templateEl.setAttribute('data-template-id', templateId);
templateEl.classList.add('template'); templateEl.classList.add('template');
templateEl.setAttribute('tabindex', '0');
templateEl.dataset.templateId = normalizedId;
templateEl.dataset.templateRu = ruText;
if (enText) {
templateEl.dataset.templateEn = enText;
}
templateEl.dataset.searchText = `${title} ${ruText} ${enText}`.toLowerCase();
templateEl.innerHTML = ` templateEl.innerHTML = `
<span id="template-title" class="font-monospace">${title}</span> <span class="font-monospace template-title">${title}</span>
<div class="bigFormochka"> <button class="template-favorite btn" type="button" title="Удалить из избранного">
<div class="formochka"> <i class="fa-solid fa-star"></i>
<input id="template-title-input" class="hidden form-control-sm"> </button>
<textarea id="template-textareaRU" class="hidden form-control-lg border-2" rows="1" cols="30.5">${RUText}</textarea>
<textarea id="template-textareaEN" class="hidden form-control-lg border-2" rows="1" cols="30.5">${ENText}</textarea>
</div>
<span class="buttonsLong"> <span class="buttonsLong">
<span class="btn btn-light" data-ru-text="${RUText}" id="template-ru-text">RU</span> ${ruText ? `<span class="btn btn-light template-ru-text" data-ru-text="${ruText}">RU</span>` : ''}
<span class="btn btn-light" data-en-text="${ENText}" id="template-en-text">ENG</span> ${enText ? `<span class="btn btn-light template-en-text" data-en-text="${enText}">ENG</span>` : ''}
<button class="template-edit btn"><i class="fa-solid fa-pen-to-square"></i></button>
<button class="template-delete btn"><i class="fa-solid fa-trash"></i></button>
</span> </span>
</div>
`; `;
const editBtn = templateEl.querySelector('.template-edit'); const favoriteBtn = templateEl.querySelector('.template-favorite');
const deleteBtn = templateEl.querySelector('.template-delete'); favoriteBtn.addEventListener('click', async (e) => {
const titleEl = templateEl.querySelector('#template-title'); e.stopPropagation();
const textRUEl = templateEl.querySelector('#template-ru-text'); favorites = await templateStorage.getFavorites();
const textENEl = templateEl.querySelector('#template-en-text'); const currentState = favorites.includes(normalizedId);
const titleInputEl = templateEl.querySelector('#template-title-input'); favorites = await templateStorage.toggleFavorite(normalizedId, !currentState);
const textRUInputEl = templateEl.querySelector('#template-textareaRU'); updateFavoritesDisplay();
const textENInputEl = templateEl.querySelector('#template-textareaEN');
// Функция для переключения видимости элементов
function toggleVisibility(...elements) {
elements.forEach(el => el.classList.toggle('hidden'));
}
// Обработчик для редактирования
editBtn.addEventListener('click', () => {
toggleVisibility(titleEl, textRUEl, textENEl, titleInputEl, textRUInputEl, textENInputEl);
}); });
// Обработчик для удаления const ruBtn = templateEl.querySelector('.template-ru-text');
deleteBtn.addEventListener('click', () => { const enBtn = templateEl.querySelector('.template-en-text');
const templates = JSON.parse(localStorage.getItem('templates')) || []; if (ruBtn) {
const templateIndex = templates.findIndex(template => template.title === titleEl.innerText); ruBtn.addEventListener('click', () => copyToClipboard(ruText));
if (templateIndex !== -1) { }
templates.splice(templateIndex, 1); if (enBtn) {
localStorage.setItem('templates', JSON.stringify(templates)); enBtn.addEventListener('click', () => copyToClipboard(enText));
} }
templateEl.remove();
});
// Обработчики для изменений в полях
const handleChange = (key, valueEl, inputEl) => {
valueEl.innerText = inputEl.value;
navigator.clipboard.writeText(valueEl);
updateTemplateInLocalStorage(title, key, inputEl.value);
};
titleInputEl.addEventListener('change', (e) => handleChange('title', titleEl, e.target));
textRUInputEl.addEventListener('change', (e) => handleChange('RUText', textRUEl, e.target));
textENInputEl.addEventListener('change', (e) => handleChange('ENText', textENEl, e.target));
// Обработчик для копирования текста в буфер обмена
const handleCopyText = (e, key) => {
navigator.clipboard.writeText(e.target.dataset[key])
.then(() => console.log(`Текст ${key} скопирован в буфер обмена`))
.catch(err => console.error(`Не удалось скопировать текст ${key}: `, err));
};
textRUEl.addEventListener('click', (e) => handleCopyText(e, 'ruText'));
textENEl.addEventListener('click', (e) => handleCopyText(e, 'enText'));
return templateEl; return templateEl;
}
function saveTemplateToLocalStorage(title, RUText, ENText) {
const template = { title, RUText, ENText };
const templates = JSON.parse(localStorage.getItem('templates')) || [];
templates.push(template);
localStorage.setItem('templates', JSON.stringify(templates));
}
function updateTemplateInLocalStorage(title, key, value) {
const templates = JSON.parse(localStorage.getItem('templates')) || [];
const templateIndex = templates.findIndex(template => template.title === title);
if (templateIndex !== -1) {
templates[templateIndex][key] = value;
localStorage.setItem('templates', JSON.stringify(templates));
}
}
addBtn.addEventListener('click', () => {
const title = "Title";
const RUText = "RUText";
const ENText = "ENText";
const el = createTemplate(title, RUText, ENText);
templatesEl.appendChild(el);
saveTemplateToLocalStorage(title, RUText, ENText);
});
window.onload = function () {
const templates = JSON.parse(localStorage.getItem('templates')) || [];
templates.forEach(template => {
const el = createTemplate(template.title, template.RUText, template.ENText);
templatesEl.appendChild(el);
});
};
//--------------------------------------------------------------------------------------------------------------------------------//
// Тема
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = document.getElementById('theme-icon');
// Применение сохраненной темы
const applySavedTheme = () => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-theme');
if (themeIcon) {
themeIcon.classList.replace('fa-moon', 'fa-sun');
}
} else {
document.body.classList.remove('dark-theme');
if (themeIcon) {
themeIcon.classList.replace('fa-sun', 'fa-moon');
}
}
}; };
// Переключение темы const updateFavoritesDisplay = async () => {
if (themeToggle && themeIcon) { if (!favoritesContainer) return;
themeToggle.addEventListener('click', () => { favorites = await templateStorage.getFavorites();
document.body.classList.toggle('dark-theme'); favoritesContainer.innerHTML = '';
const theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
localStorage.setItem('theme', theme); // Сохраняем выбранную тему const allTemplates = templateStorage.getAll();
themeIcon.classList.replace(theme === 'dark' ? 'fa-moon' : 'fa-sun', theme === 'dark' ? 'fa-sun' : 'fa-moon'); const favoriteUserTemplates = allTemplates.filter(t => favorites.includes(t.id));
favoriteUserTemplates.forEach(template => {
const element = createTemplateElement({ ...template, favorite: true }, templateCallbacks);
favoritesContainer.appendChild(element);
});
const favoritePredefinedIds = favorites.filter(id => !allTemplates.some(t => t.id === id));
const normalizedIds = [...new Set(favoritePredefinedIds.map(normalizePredefinedId))];
normalizedIds.forEach(normalizedId => {
const element = createPredefinedTemplateElement(normalizedId, clipboardTexts);
if (element) {
favoritesContainer.appendChild(element);
}
});
document.querySelectorAll('.template').forEach(templateEl => {
const templateId = templateEl.dataset.templateId;
const buttonId = templateEl.querySelector('[id]')?.id;
let id = templateId || buttonId;
if (id && !templateId) {
id = normalizePredefinedId(id);
}
if (id) {
const isFavorite = favorites.includes(id);
const favoriteBtn = templateEl.querySelector('.template-favorite');
const favoriteIcon = favoriteBtn?.querySelector('i');
if (favoriteIcon) {
favoriteIcon.className = `fa-${isFavorite ? 'solid' : 'regular'} fa-star`;
favoriteBtn.title = isFavorite ? 'Удалить из избранного' : 'Добавить в избранное';
}
templateEl.dataset.favorite = isFavorite ? 'true' : 'false';
}
});
hotkeyManager.updateAssignments();
};
if (addBtn) {
addBtn.addEventListener('click', async () => {
try {
const templateData = await templateModal.open();
if (!templateData) {
return;
}
const newTemplate = {
id: generateTemplateId(),
...templateData
};
renderTemplate(newTemplate);
await templateStorage.add(newTemplate);
updateCustomCategoryVisibility();
} catch (error) {
if (error?.message !== 'cancelled') {
console.warn('Создание шаблона отменено или недоступно.', error);
}
}
}); });
} }
// Применить сохраненную тему при загрузке страницы if (exportBtn) {
applySavedTheme(); exportBtn.addEventListener('click', async () => {
try {
const payload = {
version: 1,
exportedAt: new Date().toISOString(),
templates: templateStorage.getAll().map(template => ({ ...template }))
};
await downloadJsonFile(payload, 'templates-export.json');
} catch (error) {
console.error('Не удалось экспортировать шаблоны:', error);
alert('Не получилось сформировать файл экспорта. Попробуйте ещё раз.');
}
});
}
// Управление категориями if (importBtn) {
document.querySelectorAll('.category h3').forEach(function(header) { importBtn.addEventListener('click', async () => {
const categoryId = header.textContent.trim(); // Уникальный идентификатор категории по названию try {
const content = header.nextElementSibling; // Содержимое категории const jsonText = await importModal.open();
const icon = header.querySelector('.toggle-icon'); // Иконка стрелки if (!jsonText) {
return;
}
const importedTemplates = await importTemplatesFromText(jsonText, templateStorage);
// Проверка состояния категории при загрузке страницы importedTemplates.forEach(template => {
const isCategoryOpen = localStorage.getItem(categoryId) === 'true'; renderTemplate(template);
});
if (isCategoryOpen) { updateCustomCategoryVisibility();
content.classList.add('show'); alert(`Импортировано шаблонов: ${importedTemplates.length}`);
if (icon) { } catch (error) {
icon.classList.replace('fa-chevron-down', 'fa-chevron-up'); if (error?.message === 'cancelled') {
return;
}
console.error('Ошибка импорта:', error);
alert(`Не удалось импортировать шаблоны: ${error.message || error}`);
}
});
}
const updateCheckBtn = document.getElementById('update-check-btn');
const updateCheckStatus = document.getElementById('update-check-status');
const showUpdateStatus = (text, isError = false) => {
if (!updateCheckStatus) return;
updateCheckStatus.textContent = text;
updateCheckStatus.classList.remove('hidden', 'update-check-error');
if (isError) updateCheckStatus.classList.add('update-check-error');
updateCheckStatus.classList.remove('hidden');
setTimeout(() => updateCheckStatus.classList.add('hidden'), 5000);
};
if (updateCheckBtn) {
updateCheckBtn.addEventListener('click', async () => {
try {
if (typeof chrome !== 'undefined' && chrome.permissions?.request) {
const granted = await chrome.permissions.request({ origins: ['https://git.gorshenin.info/*'] });
if (!granted) {
showUpdateStatus('Для проверки обновлений нужен доступ к репозиторию.', true);
return;
}
}
const result = await checkForUpdate();
if (result.error) {
showUpdateStatus(`Проверка обновлений: ${result.error}`, true);
return;
}
if (result.hasUpdate) {
showUpdateStatus(`Доступна версия ${result.latest}. Открываю репозиторий…`);
if (typeof chrome !== 'undefined' && chrome.tabs?.create) {
chrome.tabs.create({ url: result.downloadUrl });
} else {
window.open(result.downloadUrl, '_blank');
} }
} else { } else {
content.classList.remove('show'); showUpdateStatus(`У вас последняя версия (${result.current}).`);
if (icon) {
icon.classList.replace('fa-chevron-up', 'fa-chevron-down');
} }
} catch (err) {
showUpdateStatus(err?.message || 'Не удалось проверить обновления', true);
}
});
} }
// Обработчик клика по заголовку категории const renderStoredTemplates = async () => {
header.addEventListener('click', function() { await templateStorage.load();
content.classList.toggle('show'); // Переключаем видимость категории favorites = await templateStorage.getFavorites();
icon.classList.toggle('fa-chevron-down'); templateStorage.getAll().forEach(template => {
icon.classList.toggle('fa-chevron-up'); renderTemplate(template);
// Сохраняем состояние категории как открытое или закрытое });
localStorage.setItem(categoryId, content.classList.contains('show') ? 'true' : 'false'); updateFavoritesDisplay();
}); updateCustomCategoryVisibility();
};
await renderStoredTemplates();
document.querySelectorAll('.template').forEach(template => {
if (!template.hasAttribute('tabindex')) {
template.setAttribute('tabindex', '0');
}
}); });
}); });

102
src/templates.json Normal file
View File

@@ -0,0 +1,102 @@
{
"clipboardTexts": {
"inWorkRu": "Коллеги, здравствуйте.\n\nПриняли Ваш запрос в работу. Сообщим по мере поступления информации.",
"inWorkEn": "Dear customer,\n\nWe are working on your request.",
"dialogueRu": "Коллеги, здравствуйте.\n\nУведомляем вас о том, что мы начали диалог с оператором по данному запросу. Мы ожидаем ответа и будем держать вас в курсе любых изменений.",
"dialogueEn": "Dear colleagues.\n\nWe would like to inform you that we have started a dialogue with the operator on this request. We are awaiting a response and will keep you informed of any changes.",
"nonDelRu": "Коллеги, здравствуйте.\n\nПросьба уточнить причину недоставки следующих сообщений клиенту:",
"nonDelEn": "Dear colleagues,\n\nPlease check why the following messages were not delivered:",
"fixItRu": "Коллеги,\n\nМы не фиксируем на нашей платформе ваших СМС-сообщений, просьба предоставить ответ от нашей платформы на ваш запрос отправки. Так же просьба предоставить логи, подтверждающие отправку на нашу платформу, данная информация необходима для дальнейшего анализа вашего запроса.",
"fixItEn": "Dear colleagues,\n\nWe do not record your SMS messages on our platform, please provide a response from our platform to your request to send. Please also provide logs confirming sending to our platform, this information is necessary for further analysis of your request.",
"fakeRu": "Коллеги, здравствуйте.\n\nПросьба уточнить корректность статусов доставки следующих сообщений клиенту. Абонент не получал СМС.",
"fakeEn": "Dear colleagues,\n\nPlease clarify the correctness of the delivery statuses of the following messages to the client. The subscriber did not receive an SMS.",
"weworkRu": "Коллеги, здравствуйте.\n\nУведомляем вас о том, что работы по вашему запросу продолжаются. Как только появится информация, мы вам сообщим.\n\nБлагодарим за понимание и приносим извинения за доставленные неудобства.",
"weworkEn": "Dear Colleagues.\n\nWe notify you that work on your request is ongoing. As soon as information becomes available, we will inform you.\n\nThank you for your understanding and we apologize for the inconvenience caused.",
"wework2Ru": "Коллеги, здравствуйте.\n\nЖдем ответа от оператора. Сообщим, как только появится соответствующая информация. Спасибо!",
"wework2En": "Dear Colleagues.\n\nWe are waiting for a response from the operator. We will inform you as soon as we have an update. Thank you!",
"wework3Ru": "Здравствуйте!\n\nОтправили повторный запрос оператору по данному поводу.\n\nСпасибо!",
"wework3En": "Hello!\n\nWe have sent an additional request to the operator.\n\nThank you!",
"theyworkRu": "Здравствуйте, коллеги.\n\nПоявилась ли информация по данному вопросу?\n\nСпасибо.",
"theyworkEn": "Dear colleagues,\n\nIs there any information on this request?\n\nThank you.",
"theywork2Ru": "Добрый день!\n\nПросим вернуться с ответом.\n\nБольшое спасибо!",
"theywork2En": "Hello Team!\n\nPlease let us know if there are any updates.\n\nThank you!",
"theywork3Ru": "Здравствуйте!\n\nПодскажите, пожалуйста, появились ли новости.\n\nСпасибо!",
"theywork3En": "Hello!\n\nPlease kindly advise on issue status.\n\nThank you.",
"delivedRu": "Коллеги,\n\nСообщения успешно прошли через платформу и были отправлены оператору. По информации от оператора, сообщения были своевременно доставлены. В случае, если абонент утверждает, что сообщения не были получены, просьба проверить исправность работы ТА. Возможные рекомендации: перезагрузка телефона, очистка памяти от устаревших сообщений, проверка спам-фильтров и черных списков, обновление ПО.\n\nПри сохранении проблемы, просьба запросить у абонента детализацию. Данная информация потребуется для дальнейшего взаимодействия с оператором.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"delivedEn": "Colleagues, according to the information from the operator, messages were delivered in a timely manner. If the subscriber claims that the messages have not been received, please check that device is working correctly. Possible recommendations: reboot the phone, clear message memory, check spam filters and blacklists, update the software.\n\nIf the problem still persists, please ask the subscriber for details. This information is necessary to continue interaction with the operator.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"problphRu": "Коллеги,\n\nСообщения успешно прошли через платформу и были отправлены оператору. По информации от оператора, сообщения не были доставлены абоненту по причине сбоя ТА. Возможные рекомендации для абонента: перезагрузка телефона, очистка памяти от устаревших сообщений, проверка спам-фильтров и черных списков, обновление ПО.\n\nПросьба уведомить нас в случае, если ситуация повторится.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"problphEn": "Colleagues, according to the information from the operator, messages were not delivered due to subscribers device failure. Possible recommendations: reboot the phone, clear message memory, check spam filters and blacklists, update the software.\n\nPlease let us know if the problem persist.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"notservRu": "Здравствуйте, коллеги.\n\nСообщения успешно прошли через платформу и были отправлены оператору. По информации от оператора, сообщения абоненту не были доставлены, т.к. номер телефона не существует или не обслуживается.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"notservEn": "Colleagues\n\nThe message successfully passed through the platform and was sent to the operator. According to the information from the operator, the messages were not delivered to the subscriber because the phone number does not exist or is not serviced.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"insentRu": "Коллеги,\n\nСообщения успешно прошли через платформу и были отправлены оператору. Сообщения находятся в статусе Отправлено, SMS-центр оператора продолжает осуществлять попытки их доставки.\n\nНаиболее вероятной причиной задержки в доставке может являться нахождение абонента в зоне неуверенного приема сети, также возможен технический сбой в работе телефона абонента.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"insentEn": "Colleagues,\n\nThe messages are in Sent status, operator's SMS center is continuing delivery attempts. Most likely, delivery delays are caused by subscriber being in the area of uncertain network reception, or a technical failure in the operation of the subscriber's phone is also possible.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"dubleRu": "Коллеги,\n\nМониторингом были зафиксированы дубликаты сообщений, в связи с чем сработала блокировка. Если в короткий промежуток времени на площадку поступает два одинаковых сообщения, повторное блокируется как дубликат.\n\nЕсли отправка дубликатов была запланирована, либо номер абонента используется для тестирования подключения, просьба сообщить, мы добавим абонента в белый список.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"dubleEn": "Colleagues, duplicates were blocked by our monitoring system. If two identical messages are received within a short period of time, the second one is blocked as a duplicate.\n\nIf sending duplicates was planned, or the subscriber's number is used for testing, please let us know and we will whitelist the subscriber.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"unvelbRu": "Коллеги,\n\nСообщения успешно прошли через платформу и были отправлены оператору. Сообщения абоненту не были доставлены т.к. ТА абонента был недоступен - выключен или находился в зоне неуверенного приема сигнала.\n\nРекомендуем абоненту перезагрузить телефон, проверить доступность сети оператора, очистить память телефона от устаревших сообщений.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"unvelbEn": "Colleagues,\n\nThe messages were not delivered to the subscriber because the subscriber's phone was unavailable - turned off or was in the zone of uncertain signal reception.\n\nWe recommend that the subscriber restart the phone, check the availability of the operator's network, and clear the phone's memory of outdated messages.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"delayRu": "Коллеги, здравствуйте.\n\nПросьба уточнить текущий статус сообщений и причины задержек:",
"delayEn": "Dear Colleagues.\n\nPlease clarify current messages status and the reasons for the delays:",
"conbreRu": "Коллеги, здравствуйте.\n\nНаблюдаем разрыв соединения. Просьба уточнить, ситуация штатная?",
"conbreEn": "Dear colleagues.\n\nWe observe a connection break. Please clarify whether its a standard situation.",
"corroutRu": "Коллеги,\n\nПо информации от оператора, для абонента были внесены корректировки в таблицы маршрутизации.\n\nПросьба проверить и сообщить в случае повторения ситуации.",
"corroutEn": "Dear colleagues,\n\nThe adjustments have been made in routing settings.\nAs our logs are now showing positive delivery results, we would like to ask you to perform a fresh check on your side and share the results in case if further assistance is required.",
"tehtroubRu": "Коллеги,\n\nПо информации от оператора, сообщения не были доставлены абоненту из-за временных технических неполадок на стороне оператора. На текущий момент доставка сообщений осуществляется в штатном режиме.\n\nПросьба проверить и сообщить в случае повторения ситуации.",
"tehtroubEn": "Dear colleagues,\n\nAccording to information we got from the operator, the messages were not delivered to the subscriber due to temporary technical issues on the operator's side. At the moment, messages are being delivered in the regular mode.",
"senderRu": "Коллеги,\n\nСообщения не были доставлены, так как для имени отправителя ######## требуется регистрация на оператора ###Имя бренда###.\n\nПросьба подать имя на регистрацию в ЛК или обратиться по данному вопросу к Вашему менеджеру.",
"senderEn": "Colleagues, the below reported traffic failed to be delivered due to an unregistered Sender ID ########. Alphanumeric registration is required towards ###COUNRTY OPERATOR (MCCMNC)###, and delivery of messages with unregistered Sender IDs is on the best effort basis. To register, please contact your Account Manager.",
"notResponseRU": "Здравствуйте!\n\nК сожалению, мы не получили от Вас ответа в течение 20 дней, поэтому запрос будет автоматически закрыт. Вы можете ответить на это письмо, если проблема не решена и требуется поддержка.",
"notResponseEn": "Hello!\n\nUnfortunately, we have not received a response from you within 20 days, so the request will be automatically closed. You can reply to this email if the problem has not been resolved and support is required.",
"fwd2treadRU": "Коллеги, здравствуйте.\n\nПередали вопрос ответственной команде. Сообщим по мере поступления информации.",
"fwd2treadEn": "Dear colleagues,\n\nWe have escalated the issue to the responsible team.\nWe will keep you informed as we receive any updates.",
"daoffice1RU": "Добрый день!\n\nДля регистрации в приложении \"КингДом\" необходимо внести Ваш номер телефона в Axapta.\n\nПо данному вопросу, пожалуйста, обратитесь к директору своего ресторана или на электронную почту: kingdom@burgerking.ru\n\nЧерез три часа после внесения номера, Вы сможете войти в сеть по номеру телефона, используя приложение \"КингДом\".",
"officeRU": "Добрый день!\n\nМы проверили Вашу учетную запись - все данные корректны.\nПроверочный СМС-код успешно отправлен на Ваш мобильный телефон.\n\nЕсли у Вас устройство на базе Android, то пожалуйста, переустановите приложение \"КингДом\" (удалить\\скачать заново) по следующей ссылке ниже, чтобы исключить отсутствие свежих обновлений: https://play.google.com/store/apps/details?id=ru.kingdom\n\nЕсли у Вас устройство на базе IOS, то пожалуйста, не удаляйте приложение \"КингДом\", так как оно было удалено из магазина приложений AppStore в связи с изменениями политики Apple Inc.\nВ настоящий момент, Вы можете пользоваться веб-версией приложения \"КингДом\" через браузер или уже установленной версий приложения.\nКоманда разработчика уже ведёт работу по восстановлению приложения.\nМы сообщим как только приложение будет доступно для скачивания в AppStore.\n\nПриложения для смартфонов на базе Android доступно для скачивания без изменений.\n\nЕсли данные действия не помогут, то, пожалуйста, опишите подробнее, на каком моменте у Вас не получается зайти в систему (при наличии ошибки приложите скриншот).",
"blockMsisdnRu": "Коллеги,\n\nДанный номер находится в глобальном ЧС.\n\nЭто означает, что клиент жаловался на получение СМС и отказался от получения всех СМС, либо были жалобы от данного абонента в УФАС.\n\nИз глобального ЧС, к сожалению, мы не можем разблокировать по Вашей просьбе, только в случае, если абонент напишет сам о просьбе разблокировать, с пометкой о согласии получения всех сообщений от оператора.\n\nЗаявление пишется в свободной форме, скан необходимо прислать на почту вашему менеджеру. Пример:\n\nКому: ООО «АйДиджитал»\nГенеральному директору: Баранову А.В.\nПрошу убрать мой номер ____________ из черного списка. Понимаю и соглашаюсь на возможное получение рассылок от других компаний, которые осуществляют рассылки через платформу АйДиджитал.\n\nПодпись и дата.",
"commonSenderRu": "Коллеги, сообщения были заблокированы согласно правилам отправки для каналов с Общим именем. А именно -\n----------------------------\n в тексте сообщения содержится упоминания международного бренда (рассылки в интересах международных брендов).\n на канале запрещена отправка сообщений, если в качестве сендера используется международный сендер\n на канале запрещена отправка сообщений на латинице\n на канале запрещена отправка сообщений с кодами в тексте которых нет упоминания сервиса от которого идет отправка\n на канале запрещена отправка сообщений в тексте которых содержится ссылка (ссылкой считается - наличие http в URL)\n на канале запрещена отправка сообщений в тексте сообщений которых указаны только цифры\n на канале запрещена отправка сообщений с менее 13 символов в тексте",
"noResponse": "Здравствуйте!\n\nК сожалению, мы не получили от Вас ответа в течение 5 рабочих дней, поэтому запрос будет автоматически закрыт.\n\nВы можете ответить на это письмо, если проблема не решена и требуется дополнительная помощь.",
"conductedTesting": "Коллеги,\n\nМы провели тестирование данного направления, используя живые номера. Все сообщения были доставлены на устройства правильно и без задержек.\n\nПросьба провести повторное тестирование со своей стороны и сообщить в случае повторения ситуации.",
"HLRRu": "Коллеги, здравствуйте.\\nПросьба уточнить причину возврата некорректных HLR статусов на указанные номера:",
"HLREn": "Dear colleagues,\n\nPlease clarify the correctness of HLR statuses for the following subscriber numbers:",
"managerTemplate": "Страна: \nОператор: \nmccMnc: \nСендер: \nКлиент: \nSource node: \nDestination node: ",
"suspiciousActivity": "Здравствуйте, коллеги!\n\nНашей системой обнаружена аномальная активность по направлению \"\".\n\nПросим, пожалуйста, проверить и сообщить является ли данная ситуация штатной.\n\nГрафик с общим количеством трафика по направлению:\n\n\nКоличество трафика по часам:\n\n\nКоличество сообщений по номерам:\n\n\nПеребор по номерам:\n\n\nПожалуйста, дайте нам знать, если у вас возникли вопросы или требуется дополнительная информация.",
"trafficBlocking": "Здравствуйте, коллеги!\n\nНашей системой обнаружена аномальная активность по направлению \"\".\n\nУведомляем вас о том, что трафик по данному направлению был ЗАБЛОКИРОВАН. Просим, пожалуйста, проверить и сообщить является ли данная ситуация штатной.\n\nГрафик с общим количеством трафика по направлению:\n\n\nКоличество трафика по часам:\n\n\nКоличество сообщений по номерам:\n\n\nПеребор по номерам:\n\n\nПожалуйста, дайте нам знать, если у вас возникли вопросы или требуется дополнительная информация.",
"nonDelPushRu": "Коллеги,\n\nПуш-сообщения не были доставлены по причине истечения TTL. ТА абонента не вернул статус получения пуш-сообщения в установленное время, поэтому сообщению присвоен статус UNDELIVERED. \n\nВозможными причинами недоставки могут быть: отсутствие подключения к интернету, режим энергосбережения или \"тихий\" режим, на устройстве включен \"Режим полета\", отсутствие прав у приложения на отправку уведомлений.",
"nonDelPushEng": "Colleagues,\n\nPush messages were not delivered due to TTL expiration. The subscriber's device did not return the status of receiving the push message at the set time, so the message was assigned the status UNDELIVERED. \n\nPossible reasons for non-delivery may be: lack of Internet connection, power saving mode or \"silent\" mode, Airplane Mode is enabled on the device, and the app does not have rights to send notifications.",
"riveGaucheStaff": "Здравствуйте, коллеги!\n\nНашей системой обнаружена аномальная активность по направлению \"УКАЗАТЬ БРЕНД И СТРАНУ\".\n\nПросим, пожалуйста, проверить и сообщить является ли данная ситуация штатной.\n\nЦРТ -\n\nГрафик с общим количеством трафика по направлению:\n\nКоличество трафика по часам:\n\nКоличество сообщений по номерам:\n\nПеребор по номерам:\n\nПожалуйста, дайте нам знать, если у вас возникли вопросы или требуется дополнительная информация.",
"riveGaucheBlock": "Здравствуйте, коллеги!\n\nНашей системой обнаружена аномальная активность по направлению \"УКАЗАТЬ БРЕНД И СТРАНУ\".\n\nУведомляем вас о том, что трафик по данному направлению был ЗАБЛОКИРОВАН. Просим, пожалуйста, проверить и сообщить является ли данная ситуация штатной.\n\nЦРТ - \n\nГрафик с общим количеством трафика по направлению:\n\nКоличество трафика по часам:\n\nКоличество сообщений по номерам:\n\nПеребор по номерам:\n\nПожалуйста, дайте нам знать, если у вас возникли вопросы или требуется дополнительная информация.",
"ahtungDefault": "Направление: \nПроблема: 100% недоставка | задержки\nЗапрос оператору: \nКого затронуло: \n\nАхтунг: направили\nВремя начала: \nОтветственный за решение инцидента: ",
"504Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. сервис коротких сообщений не предоставляется абоненту.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"504En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the short message service is not provided to the subscriber.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"505Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения абоненту не были доставлены, т.к. у абонента включен запрет на прием смс сообщений или абонент заблокирован оператором (возможно, в связи с отрицательным балансом).\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"505En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber has a ban on receiving messages or the subscriber was blocked by the operator (possibly due to a negative balance).\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"506Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. отправка смс сообщений на указанный номер недоступна. Причиной ограничения может быть блокировка входящих смс сообщений на телефоне, либо полное отключение услуги.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"506En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: sending SMS messages to the specified number is unacceptable. The reasons for the restriction may be blocking incoming SMS messages on the phone, or completely disabling the service.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"508Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения абоненту не были доставлены т.к. ТА абонента был недоступен - выключен или находился в зоне неуверенного приема сигнала.\nРекомендуем абоненту перезагрузить телефон, проверить доступность сети оператора, очистить память телефона от устаревших сообщений.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"508En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's phone was unavailable - turned off or was in the zone of uncertain signal reception.\nWe recommend that the subscriber restart the phone, check the availability of the operator's network, and clear the phone's memory of outdated messages.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"509Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. абонент находится в роуминге. У оператора, который доставляет сообщение, нет соглашения на роуминг с роуминг-оператором.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"509En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber is roaming, but the operator delivering the message does not have a roaming agreement with the roaming operator.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"510Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. на момент отправки сообщений абонентом была произведена замена SIM-карты. В течение 24-х часов с момента замены SIM-карты могут наблюдаться трудности в доставке смс сообщений.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"510En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: at the time of sending the messages, the subscriber had replaced the SIM card. The operator rejects SMS messages within 24 hours from the moment the SIM card is replaced.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"511Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. очередь сообщений для абонента со стороны оператора переполнена, в связи с чем оператор отклонил сообщение.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"511En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the message was rejected by the operator due to a full message queue on the recipient's side.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"515Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. на попытки доставки сообщений до ТА абонента возвращается ошибка, которая свидетельствует о некорректной работе ТА в сети или нехватке памяти на ТА абонента.\n\nАбоненту рекомендуется:\n1. Очистить память аппарата от устаревших сообщений\n2. Проверить свой аппарат на наличие сбоев, перезагрузить аппарат, проверить черные списки и спам-фильтры аппарата\n3. Удалить стороннее ПО для работы с sms, установленное на аппарате\n4. При необходимости провести обновление системного ПО, или сброс настроек аппарата к заводским.\n5. Также абоненту рекомендуется протестировать прием данных сообщений, переставив sim-карту в другой, заведомо исправный аппарат.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"515En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our платформ to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: when attempts are made to deliver messages to the subscriber, an error is returned, which indicates that subscriber's phone is not working correctly on the network or that there is a lack of memory on the subscriber.\n\nWe recommend the subscriber to:\n1. Clear the handset memory of out-of-date messages;\n2. Check the handset for failures, reboot the device, check blacklists and spam filters\n3. Remove third-party software for working with sms installed on the device;\n4. If necessary, update the system software, or reset the device to original settings;\n5. Test the reception by moving the SIM card to another device known to be working.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"517Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. на попытки доставки сообщений до ТА абонента возвращается ошибка, которая свидетельствует о некорректной работе ТА в сети или нехватке памяти на ТА абонента.\n\nАбоненту рекомендуется:\n1. Очистить память аппарата от устаревших сообщений\n2. Проверить свой аппарат на наличие сбоев, перезагрузить аппарат, проверить черные списки и спам-фильтры аппарата\n3. Удалить стороннее ПО для работы с sms, установленное на аппарате\n4. При необходимости провести обновление системного ПО, или сброс настроек аппарата к заводским.\n5. Также абоненту рекомендуется протестировать прием данных сообщений, переставив sim-карту в другой, заведомо исправный аппарат.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"517En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: when attempts are made to deliver messages to the subscriber, an error is returned, which indicates that subscriber's phone is not working correctly on the network or that there is a lack of memory on the subscriber.\n\nWe recommend the subscriber to:\n1. Clear the handset memory of out-of-date messages;\n2. Check the handset for failures, reboot the device, check blacklists and spam filters\n3. Remove third-party software for working with sms installed on the device;\n4. If necessary, update the system software, or reset the device to original settings;\n5. Test the reception by moving the SIM card to another device known to be working.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"523Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. ТА абонента занят операцией, препятствующей получению короткого сообщения.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"523En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's phone is busy with an operation that prevents the receipt of a short message.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"526Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. ТА абонента (IMEI) не определен или не разрешен для использования в сети оператора.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"526En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's device (IMEI) is not recognized or is not authorized for use on the operator's network.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"527Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. ТА абонента был недоступен - выключен или находился в зоне неуверенного приема сигнала.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"527En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our platform to the оператор. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's phone was unavailable - turned off or was in an area of uncertain signal reception.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"529MTSRu": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. у получателя активирована услуга «Запрет на SMS информирование».\n\nДанная опция блокирует массовые рассылки - зачастую это рекламные рассылки, опросы или уведомления от магазинов. Однако в зону блокировки могут попасть и уведомления от банков, сервисов авторизации или бронирования. Чтобы начать вновь получать сообщения, получателю необходимо обратиться к своему оператору связи и отключить услугу «Запрет на SMS информирование». После этого доставка SMS будет восстановлена.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"529MTSEn": "Dear partner,\n\nWe confirm that the message was successfully handed off from our платформ to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the recipient has activated the \"Ban on SMS notification\" service.\n\nThis option blocks mass mailings, which are often promotional mailings, surveys, or notifications from stores. However, notifications from banks, authorization services, or booking services may also be blocked. To start receiving messages again, the recipient needs to contact their telecom operator and disable the \"Ban on SMS notification\" service. After that, SMS delivery will be restored.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"529MegaRu": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. был указан некорректный номер абонента, либо нет правил маршрутизации для данного абонента или ошибка при отправке на блокированные номера (по закону Кудрявцева).\n\nУточняем что с 1 июня вступили в силу поправки в Федеральный закон \"О связи\", касающиеся в основном регистрации корпоративных SIM-карт и противодействия незаконной реализации SIM-карт.\nТеперь юридические лица и индивидуальные предприниматели обязаны предоставлять операторам связи и на \"Госуслугах\" данные о сотрудниках, использующих корпоративные SIM-карты, а также о номерах в устройствах M2M/IoT. Возможные рекомендации: проверка своих паспортных данных на госуслугах, привязка номера телефона к действующему аккаунту на госуслугах.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"529MegaEn": "Dear partner,\n\nWe confirm that the message was successfully handed off from our платформ to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: an incorrect subscriber number was specified, or there are no routing rules for this subscriber, or an error was made when sending to blocked numbers (according to Kudryavtsev's law).\n\nWe would like to clarify that on June 1, amendments to the Federal Law \"On Communications\" came into force, mainly concerning the registration of corporate SIM cards and countering the illegal sale of SIM cards.\nNow, legal entities and individual entrepreneurs are required to provide telecom operators and Public Services with information about employees using corporate SIM cards, as well as numbers in M2M/IoT devices. Possible recommendations: checking your passport data on public services, linking your phone number to your current account on public services.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"532Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. были зафиксированы дубликаты сообщений, в связи с чем сработала блокировка.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"532En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our платформ to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: duplicate messages were recorded, and therefore the lock was triggered.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"561Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. номер абонента некорректен или принадлежит стационарному телефону.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"561En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our платформ to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: the subscriber's number is incorrect.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes",
"575Ru": "Здравствуйте, коллеги!\n\nСообщение успешно прошло через платформу и было отправлено оператору. По информации от оператора, сообщения не были доставлены, т.к. в целях защиты абонента от мошеннических и противоправных действий на номере на текущий момент установлены ограничения на приём входящих смс. Доставка смс будет возобновлена после снятия ограничений.\n\nБолее подробную информацию о кодах ошибок и статусах доставки вы можете найти в нашей документации: https://api.docs.direct.i-dgtl.ru/extra/references/error-codes",
"575En": "Dear partner,\n\nWe confirm that the message was successfully handed off from our платформ to the operator. According to the final delivery report from the operator, the message could not be delivered.\n\nThe reported cause was: In order to protect the subscriber from fraudulent and illegal actions, the number currently has restrictions on receiving incoming SMS messages. SMS delivery will be resumed after the restrictions are lifted.\n\nYou can find more detailed information regarding error codes and delivery statuses in our documentation: https://smsc.revicom.ltd/how-to-work-with-smsc/reserved-error-codes"
}
}

19
update.bat Normal file
View File

@@ -0,0 +1,19 @@
@echo off
chcp 65001 >nul
setlocal
cd /d "%~dp0"
echo Izipus — обновление из репозитория
echo.
git pull
if errorlevel 1 (
echo.
echo Ошибка: git pull не выполнен. Проверьте, что git установлен и папка — клон репозитория.
pause
exit /b 1
)
echo.
echo Готово. Обновите расширение в chrome://extensions (кнопка «Обновить» у Izipus).
pause

14
update.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
cd "$(dirname "$0")"
echo "Izipus — обновление из репозитория"
echo
if ! git pull; then
echo
echo "Ошибка: git pull не выполнен. Проверьте, что git установлен и папка — клон репозитория."
exit 1
fi
echo
echo "Готово. Обновите расширение в chrome://extensions (кнопка «Обновить» у Izipus)."