Адмінський редактор пісень
«Решта адмінських сценаріїв» описує договір ендпоінтів — тіла запитів, привілеї, коди помилок. Ця сторінка про інше: як влаштований сам редактор у admin/songEditor/ — щоб відтворити порівнянну функціональність, а не лише достукатись до тих самих роутів.
Бібліотека
Модалка відкривається без вибраного треку — спершу список. Одним заходом тягнуться дві речі: GET /api/admin/lyrics/songs (усі треки зі станом тексту — synced / plain / none) і GET /api/admin/lyrics/offsets (мапа зсувів за ключем виконавець+назва). Список фільтрується локально, без повторних запитів до сервера:
- пошук за словами в назві й виконавці (кожен токен має збігтися незалежно від порядку);
- перемикач «лише без синхронізації»;
- у режимі день/ніч — фільтр за мовленнєвим режимом треку.
Показ — сторінками по 5 (SEARCH_PAGE), кнопка «показати ще» довантажує наступну пачку з уже завантаженого масиву, а не з сервера: бібліотека тягнеться одним запитом цілком.
Множинний вибір
Довге натискання (утримання ~1 с) або правий клік на треку вмикає режим вибору — чекбокси замість звичайного кліку-відкриття. У цьому режимі доступні дві групові дії, і обидві йдуть через BulkConfirmModal:
- Видалення —
POST /api/admin/song-editor/batch-delete. Якщо позначено геть усе в бібліотеці, список пісень у підтвердженні ховається (він все одно не поміститься), і кнопку блокує зворотний відлік 15 секунд, перш ніж дозволити підтвердити. - Перенесення день↔ніч —
POST /api/admin/song-editor/batch-move, доступне лише коли позначені треки належать одному режиму мовлення: мішаний вибір дає підказку замість дії, а не мовчазну відмову.
В обох випадках сервер може пропустити частину треків (зайняті в ефірі — див. нижче), тож клієнт звіряє results[].ok і показує окремий тост на часткову невдачу, а не єдиний «готово».
Групове оновлення текстів
Після вибору пісень кнопка «Оновити тексти» (для однієї — «Оновити текст») відкриває BulkLyricsUpdateModal. Кнопка доступна адміністраторам із editor_lyrics, коли провайдери підтримують збереження через редактор пісень. Список показується по 10 пісень; «показати ще» відкриває наступні 10. Перед запуском ConfirmDialog попереджає про втрату поточних текстів.
Без прапорця «Форсоване оновлення» зберігається лише покращення статусу: none → plain, none → synced або plain → synced. Однаковий чи гірший статус дає «Текст не змінено» (жовтий). Форсований режим замінює наявний текст будь-яким знайденим, зокрема синхронізований звичайним. Якщо текст не знайдено, поточний текст зберігається навіть у форсованому режимі.
Обробка послідовна для всіх обраних пісень, включно з ще не показаними у списку. Праворуч від назви відображається «Пошук тексту…», потім результат: звичайний текст — фіолетовий, синхронізований — зелений, не знайдено — червоний. Помилка пошуку чи збереження позначається окремо; інші пісні продовжують оброблятися. Модалка закривається хрестиком у правому верхньому куті, а не кліком по фону. Під час обробки хрестик заблокований.
Для кожної пісні читається актуальний GET /api/admin/lyrics/cache-entry, виконується пошук через POST /api/admin/upload-song-lyrics, а потрібні зміни зберігаються через POST /api/admin/song-editor/save із lyricsChanged: true. Метадані та зсув не змінюються; блокування поточного й наступного треку перевіряє той самий серверний механізм, що й при ручному збереженні. Пошук текстів допускає upload_songs або editor_lyrics; саме editor_meta без цих прав доступу до пошуку не дає.
Картка пісні
Вибір треку зі списку вантажить GET /api/admin/lyrics/cache-entry і показує SongCard — до натискання «редагувати» це лише прев'ю: перші кілька рядків тексту (LyricsLine) без таймкодів у не-синхронізованому режимі, з ними — у синхронізованому.
Три незалежні прапорці збереження
Кнопка «зберегти» рахує, що саме реально змінилось, і шле лише це — metadataChanged, lyricsChanged (в коді — entryChanged), offsetChanged. Порівняння точне: новий текстовий запис серіалізується й звіряється з початковим побайтово, а не «чи натискали ви щось у полі». Якщо жодна з трьох речей не змінилась — запит на сервер узагалі не йде, лише тост «нічого не змінилось».
Перенесення дня/ночі — окремий, четвертий виклик (POST /api/admin/song-editor/move-mode), і йде він після основного збереження, послідовно, бо міняє songId (шлях залежить від режиму), яким має оперувати другий запит.
409 треба ловити, а не просто показувати generic-помилку
Трек, що зараз звучить або стоїть наступним, редагувати не можна — сервер віддає 409 з локалізованим поясненням у error.localized. Редактор показує це поле як є (pickLocalized), а не власний текст «щось пішло не так»; те саме стосується 403, коли надіслано *Changed: true для поля, на яке в адміна немає привілею.
Формат синхронізованого тексту
Рядок синхронізованого тексту — це LRC-подібний формат [хвилини:секунди.соті] текст, перевірений регексом /^\[(\d{2}):(\d{2})\.(\d{2,3})\] ?(.*)$/. Перед збереженням клієнт сам перевіряє, що кожен непорожній рядок відповідає формату (isSyncedFormat) — якщо ні, запит на сервер не йде взагалі, редактор одразу показує помилку формату. Соті можуть бути двома або трьома цифрами (SS або SSS) — старі імпортовані тексти інколи мають мілісекунди замість сотих, парсер приймає обидва варіанти.
Перемикання «синхронізовано» в обидва боки не втрачає дані безслідно:
- вимкнути синхронізацію — таймкоди просто зрізаються (
stripTimecodes), текст рядків лишається; - увімкнути на тексті, де таймкодів ще нема — з'являється підтвердження «згенерувати таймкоди автоматично?». Згода розставляє по одному рядку на секунду поспіль (
generateTimecodes) як чорновий каркас, відмова просто вмикає режим на голому тексті — редагувати таймкоди вручну доведеться в обох випадках, різниця лише у стартовій точці.
Хвиля і мітка часу (WaveformPlayer)
Аудіо для звірки береться з GET /api/admin/lyrics/audio-preview, який повертає посилання (тимчасове на бакет у хмарі, локально — стрім-роут із токеном), а не сам файл. Хвиля малюється по-справжньому: аудіо декодується через Web Audio (decodeAudioToWave — той самий утиль, що й у редакторі джинглів) у 600 стовпчиків RMS-амплітуди. Якщо AudioContext недоступний або файл не відкрився, малюється процедурний псевдо-візерунок замість хвилі — це позначено окремим станом (audioAvailable: false), а не тихим порожнім полотном.
Ключове: плейхед і мітка — різні речі. Плейхед (біла лінія) — це позиція відтворення, її можна тягнути по канвасу як звичайний повзунок. Мітка (жовтогаряча, зі стрілкою зверху) — незалежна позначка часу, яку виставляють поруч із рядком тексту в SyncedTextarea: клацання на стрілку біля рядка переносить мітку на його таймкод, а не запускає відтворення. Це розділення дозволяє слухати навколо одного місця (рухаючи плейхед), водночас тримаючи орієнтир, куди саме вписати наступний таймкод.
Клавіатурні скорочення (працюють, лише коли фокус не в текстовому полі): Пробіл — відтворити/пауза, ←/→ — зсунути мітку на 0.1 с, з Shift — на цілу секунду.
Порядковий редактор рядків (SyncedTextarea)
У синхронізованому режимі текст редагується не одним <textarea>, а рядок-за-рядком: кожен рядок — окремий таймкод-чіп плюс текстове поле.
- Клік на чіп відкриває його на редагування як голий
[мм:сс.сс];Enterзастосовує,Escapeскасовує. - Зміна хвилини одного таймкоду переносить нову хвилину на всі наступні рядки (
propagateMinuteForward). Це не масове перезаписування — лише компонент хвилини, секунди й соті кожного наступного рядка лишаються своїми. Розрахований на типову ситуацію: помітили, що з якогось моменту пісні таймкоди «поїхали» на цілу хвилину, і не хочеться перебивати кожен рядок вручну. Enterу текстовому полі рядка вставляє новий порожній рядок одразу під ним, з таким самим порожнім таймкодом-заглушкою[00:00.00], якщо попередній рядок був синхронізованим.- Вставка багаторядкового тексту (Ctrl+V) розбиває його на окремі рядки редактора; якщо перший вставлений рядок сам не має таймкоду, йому підставляється таймкод рядка, у який вставляли.