track == null почти не выполнялось: «безымянные» станции шлют ICY-строку без
разделителя → parseIcyTitle делает трек с пустым artist. Показываем кнопку, когда
нет РЕАЛЬНОГО трека (track null ИЛИ пустой artist/song ИЛИ song == имя станции).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- кнопка распознавания в плеере: видна только на музыкальных станциях без
метаданных эфира (track == null), показывает спиннер и результат через Toast
- распознанный трек отображается в плеере и пишется в ОТДЕЛЬНУЮ историю
распознанных (не дублируется в историю эфирных треков — гейт по ключу)
- экран Истории: переключатель «Треки эфира | Распознанные», два списка
- Room: таблица recognized_track (миграция 7→8), DAO/репозиторий
- ShazamRepository → POST /shazam/recognize/{stationId}, маппинг 503/400 в текст
- MusicGenres.isMusicStation — клиентский гейт (синхронизирован с бэкендом)
- bump backend submodule (модуль shazam)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Баг: offset первого чипа в покое = 0 (считается от начала контента, не от вьюпорта),
а формула ждала offset≥235px → «Все» и соседние были невидимы/крошечные через пол-экрана.
Фикс: recedeFactor = info.offset / shrinkPx + 1 — полный размер в покое и правее (offset≥0),
уменьшение/затухание ТОЛЬКО когда чип уезжает влево под кнопку (offset<0, зона ~32dp).
Отступ-зазор уменьшен 96→60dp (Радио) / 100→64dp (Чарты) — «Все» вплотную к кнопке.
Вместо простого затухания края — per-chip трансформация по позиции: чип масштабируется
0.42..1 + alpha по recedeFactor (offset элемента в LazyRow: правее 92dp — норма, к 46dp —
исчез). transformOrigin центр-лево → будто уходит в глубину под кнопку-категорию.
Отступ слева увеличен (взлётка под кнопкой 96/100dp). Радио и Чарты.
Кнопка-категории теперь ПОВЕРХ чипов (Box-оверлей), чипы идут во всю ширину с
отступом слева под кнопку. У левого края — затухание прозрачности (Modifier.
fadingStartEdge: graphicsLayer Offscreen + horizontalGradient BlendMode.DstIn), так
чипы при прокрутке влево красиво уплывают под кнопку и растворяются, а не обрезаются.
FilterChips/GenreSelector получили параметр contentPadding. Экраны Радио и Чарты.
- Кнопка-«категории» (круглая, акцентная рамка, иконка SlidersHorizontal) СЛЕВА от
чипа «Все» — на экранах Радио и Чарты. Открывает шторку со списком всех категорий
(Радио — жанры, Чарты — стили) + поиск, чтобы не листать чипы. CategoryPicker —
переиспользуемый компонент с поиском и отметкой выбранного.
- SearchBar: анимированная кнопка очистки (X, scale+fade появление, haptic) при
непустом запросе.
- Порядок нижнего меню: Радио · Избранное · История · Чарты · Запись · Настройки.
- Иконка вкладки при выборе делает упругий scale-«поп» (spring MediumBouncy) —
в нижнем баре и боковом рейле.
- На экране «Избранное» играющая станция теперь подсвечивается так же, как на
главной: вращающееся свечение под обложкой + индикатор-эквалайзер в углу
(FavoritesViewModel отдаёт playingStationId/isPlaying из PlayerController,
FavoritesScreen передаёт isCurrent/isPlaying в StationCard).
- Подложка adaptive-иконки: градиент под акцент темы + радиальное свечение + мягкая
тень от логотипа (ic_bg_<тема>, было плоским цветом). Иконку-лого не трогал.
- Сплэш под выбранную тему: системный сплэш Android 12+ нельзя перекрасить под выбор
пользователя (alias-тема на ColorOS игнорится), поэтому системный = просто тёмный
(splash_transparent), а красивый сплэш рисуем сами на Compose (SplashOverlay):
3D-лого + акцентное свечение + тень + анимация, цвет берём из текущей темы.
- Тему на старте читаем синхронно из SharedPreferences (мгновенно, без блокировки кадра).
- Ускорен холодный старт до первого кадра 1.48с→1.11с: сплэш рисуется на первом
дешёвом кадре, тяжёлый контент (ViewModels/плеер) композится под ним; старт
PlayerService уведён с критического пути. Остаток — оверхед debug-сборки.
- ic_fg_<тема> уменьшены (196/432) — у логотипа появились поля от краёв подложки.
- splash: windowSplashScreenAnimatedIcon → @drawable/splash_logo (новая монограмма,
forest-цвет, т.к. сплэш показывается до загрузки темы); было @mipmap/ic_launcher.
- дефолтная ic_launcher/round переведена на новый forest-логотип (недавние/настройки).
Логотип: монограмма-R пользователя отрендерена в матовый 3D через routerai
(gpt-5.4-image), один мастер перекрашен под 8 тем (recolor по яркости, форма
идентична).
- Внутри приложения: AppMark показывает перекрашенный 3D-логотип текущей палитры
(LocalThemePalette + ThemePalette.logoRes, drawable logo_<тема>).
- Иконка лаунчера следует теме: 8 adaptive-иконок (ic_fg_<тема> + ic_bg_<тема>) и
8 activity-alias в манифесте; LauncherIconManager включает alias выбранной темы,
гасит остальные (ровно один активен, guard против лишних миганий). Переключение —
в MainActivity по LaunchedEffect(paletteId). На ColorOS иконка может обновляться
с задержкой — особенность системы.
Скрипты генерации в design/logos (ключ routerai — вне репо, ~/.routerai_key).
На OnePlus/ColorOS радио глохло в фоне даже с wake mode. Причины: POST_NOTIFICATIONS
не выдан (медиа-уведомление не показывалось → foreground-сервис хрупкий) и
приложение не в вайтлисте Doze. MainActivity на старте запрашивает POST_NOTIFICATIONS
(13+), затем системный диалог REQUEST_IGNORE_BATTERY_OPTIMIZATIONS (один раз).
v1.4 / versionCode 5 (clean-сборка).
Причина петли: при bump 2→3 инкрементальная сборка НЕ пересобрала BuildConfig.dex —
в v3 APK manifest versionCode=3, но BuildConfig.VERSION_CODE остался 2. Приложение
сравнивало manifest(3) > BuildConfig(2) → всегда предлагало обновиться, ставило
тот же v3, снова предлагало. Фикс: ./gradlew clean перед сборкой релиза —
BuildConfig теперь совпадает с versionCode. Проверено на телефоне: v4, диалога нет.
Симптом: по Bluetooth в машине с выключенным экраном радио через время замолкало.
Причины и фиксы:
- setWakeMode(C.WAKE_MODE_NETWORK) + право WAKE_LOCK — ExoPlayer держит partial
wakelock + wifilock во время игры. Без этого система усыпляла CPU/Wi-Fi при
выключенном экране → буфер пустел → поток глох (главная причина).
- onPlayerError → scheduleReconnect(): при обрыве сети (туннели, край соты) поток
пере-готавливается с нарастающей задержкой (2с→15с, до 10 попыток), а не
замолкает навсегда. Счётчик сбрасывается при успешном старте; переподключение
отменяется при ручной паузе/стопе/смене станции.
Раньше пресет эквалайзера в настройках был косметикой (лежал в DataStore, к звуку
не подключён). Теперь — реальные системные эффекты на фикс. аудиосессии плеера:
- AudioEffectsController: графический Equalizer (полосы устройства), BassBoost,
Virtualizer (объём), LoudnessEnhancer (громкость тихих, до +12 дБ). Привязка к
generateAudioSessionId() в PlayerController, переживает смену станций. Применение
в реальном времени, сохранение в DataStore (commit на отпускании слайдера).
- Отдельный экран EqualizerScreen (Настройки → ЗВУК → Эквалайзер): тумблер,
системные пресеты + «Свой», слайдеры полос (±дБ), bass/virtualizer/loudness.
- Эффекты best-effort: при отсутствии поддержки блок недоступен (null), UI скрывает.
- Убран фейковый чип-пресет Flat/Rock/Pop/Jazz/Bass.
UpdateManager: на старте дёргает /app-version, при version_code > BuildConfig.
VERSION_CODE показывает UpdateDialog. Скачивает APK во внутр. Download, сверяет
SHA-256 (защита от подмены по HTTP/битой загрузки), ставит через системный
установщик (FileProvider). force_update делает диалог необкрываемым. versionCode
1→2, versionName 1.0→1.1. Добавлено право REQUEST_INSTALL_PACKAGES, путь в file_paths.
8 тёмных палитр (Лес/Океан/Закат/Аметист/Неон/Янтарь/Лёд/Роза) в Palettes.kt.
RadiolaColors теперь несёт все токены + градиент; RadiolaTheme(palette) строит из
неё и RadiolaColors, и Material ColorScheme — всё приложение берёт цвета через
RadiolaTheme.colors, поэтому смена палитры перекрашивает мгновенно. Бренд-марка
(AppMark/Wordmark) тоже следует теме. Выбор в DataStore (theme_palette, дефолт
forest), читается в MainActivity и подаётся в тему. Секция «ТЕМА ОФОРМЛЕНИЯ» в
настройках — горизонтальный ряд свотчей с превью (фон+акцент+градиент).
Было: при таймере со звуком радио кроссфейдилось в шум в НАЧАЛЕ (CROSSFADE_MS=90с),
и для 15-мин таймера уже через ~1.5 мин играл полный белый шум всё оставшееся время.
Стало: радио играет почти весь таймер; в последние SOUND_OUTRO_MS (3 мин, но не
больше половины таймера) включается звук сна — радио кроссфейдится в шум, шум держится,
в самом конце затухает в тишину. Генератор шума стартует лениво (только в аутро, не
молотит весь таймер). Засыпаешь под радио, а не под резкий шум сразу.
- RecordingPlaybackController: ExoPlayer создаётся лениво (на первый play) и
освобождается в stop() — раньше второй плеер висел в памяти всю сессию у каждого.
Поллер позиции 500мс крутится только во время игры (был вечный 2 Гц main-loop).
- StationRepositoryImpl.refreshStations: merge каталога O(n) через apiById/apiByName
индексы вместо .find на каждую станцию (было O(n²) ~700×700 на холодном старте).
Убраны verbose Log.d-трейсы (оставлен Log.e/.w на ошибки).
- Track/Station/StreamQuality помечены @Immutable — read-only модели, иначе
списки tags/qualities делали класс нестабильным → лишние рекомпозиции списков.
- HttpLoggingInterceptor только при BuildConfig.DEBUG (включён buildConfig feature):
в релизе нет оверхеда на каждый запрос и утечки URL в logcat.
- AudioSpectrumAnalyzer: FFT считается ТОЛЬКО когда открыт плеер (флаг active);
раньше ~86 FFT/с молотили всегда при проигрывании (даже экран выкл) — главный
пожиратель батареи. Включается из VisualizerHost через DisposableEffect.
- Спектр (45/с) собирается в leaf VisualizerHost, а не на верху PlayerBottomSheet —
весь плеер больше не рекомпозится 45 раз/сек.
- now-playing поллинг (5с) останавливается на паузе (isPlaying.collectLatest) —
раньше на паузе зря дёргали сеть каждые 5с.
- PlayerService.onDestroy отменяет serviceScope (singleton-плеер НЕ релизим).
- refreshStations (парс ~700 станций + сеть + Room) уведён на Dispatchers.IO с
главного потока (jank/ANR на старте).
- Coil ImageLoader: память 25% + диск 100МБ (обложки не перекачиваются каждую сессию).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Будильник (Settings → Будильник): несколько будильников, время, станция, дни недели,
fade-in пробуждения. AlarmManager.setAlarmClock (вне doze) + фолбэк, BootReceiver
перепланирует после перезагрузки, AlarmReceiver→PlayerService (foreground) →
PlayerController.startAlarmPlayback (нарастание громкости). Room: AlarmEntity/Dao, БД v7.
Выбор битрейта по умолчанию в Settings (Авто/Эконом/Стандарт/Высокое) → preferredBitrate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SleepSoundPlayer — процедурная генерация цветного шума через AudioTrack (розовый —
фильтр Келлета, коричневый — random walk). В таймере сна выбор звука: радио плавно
перетекает в выбранный шум (кроссфейд ≤90с), шум играет, к концу затухает — как в
спеке («Smart Sleep Fade»). В шторке таймера — чипы выбора звука.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
P0-фича из спеки. PlayerController: startSleepTimer/cancelSleepTimer — в последние
20с экспоненциальный fade-out громкости (frac^2), затем пауза + возврат громкости.
В плеере — пилюля «Таймер сна» (иконка Moon): при активном показывает остаток
M:SS акцентом. Шторка с интервалами 15/30/45/60/90/120 мин + «Выключить».
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Christmas Chill и др. остаются в разделе Radio Record (genre=Radio Record),
просто скрыт сам тег-чип через hiddenTags.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
У части станций Record поле stream_128 ведёт на мёртвый маунт {prefix}64.aacp
(404) — обложка/трек есть, а поток молчит (Summer Lounge, Beach Party, Reggae,
Mashup, Afro House, Nu Dance, Workout, Gop FM…). Поле stream_320 ({prefix}96.aacp)
живо у всех. Сменён приоритет выбора потока на stream_320.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record API кладёт категории станции в поле "genre" (массив {id,name}), а
StationDto/ApiMapper читали только "tags" (у станции отсутствует) → у всех
станций Рекорда жанровые теги были пустыми, и чипы вроде «Лето» при клике
показывали пустоту. Добавлено поле genres (@SerialName genre), маплю genre+tags
в Station.tags. Раздел «Лето» теперь наполняется летними станциями Record
(Chill House, Beach Party, Tropical, Summer Lounge, Mashup и др.).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Многострочные stream_url (url1\nurl2) рвали воспроизведение и health-check.
Почищены на одиночный HTTPS-поток AzuraCast (16 каналов).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- боковой nav-rail слева вместо нижнего бара в альбоме (SideNavRail)
- мини-плеер уезжает под контент в альбомной раскладке
- плеер эфира: двухпанельный (обложка слева, инфо/эквалайзер/контролы справа)
- плеер записи: слева управление, справа прокручиваемый список треков
- сетки станций и избранного: 4 колонки в альбоме вместо 2
- хелпер isLandscape() через LocalConfiguration
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Корень: NowPlayingSocketClient копит трек по станции и не чистит; combine
предпочитал socket (socketMap[id] ?: restMap[id]). Если сокет один раз
прислал трек и отвалился, залипшее значение НАВСЕГДА затеняло свежий REST —
на открытом плеере трек/обложка не менялись (Radio Record и др.). Теперь
приоритет REST (он регулярно поллится), socket — фолбэк. Поллинг плеера
ускорен 10с→5с.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bug1: плеер записи (singleton ExoPlayer) не глушился при закрытии шторки и
уходе с экрана → аудио-сирота без управления, запуск радио конфликтовал.
Теперь воспроизведение записи останавливается на onDismiss и onDispose
экрана записей, а старт радио глушит плеер записи (взаимоисключение).
Bug2: обложка/трек на открытом плеере обновлялись с задержкой при записи.
Эмиссия спектра ограничена ~45/с (было ~86/с) — меньше перегруз перерисовки;
поллинг now-playing в захвате маркеров ускорен 15с→8с (точнее тайм-коды).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Визуализатор отставал/висел: медленный спад (бар держался ~300мс после
удара) и редкие обновления. Теперь FFT с перекрытием 50% (~43 обновл/с),
мгновенный рост на удар и быстрый спад (0.78→0.55), убран искусственный
троттл 33мс. Реакция плотнее попадает в бит.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Эквалайзер почти не двигался: лог-маппинг схлопывал низы в 1-2 бина,
нормализация была слабой (двигались лишь правые полосы). Переделано:
FFT 1024→2048 (разрешение низов), полосы по частотам 40Гц-16кГц со
средним по бинам, автогейн по бегущему пику (всегда полная высота),
перцептивный лифт (sqrt) + лёгкий подъём верхов. Теперь реагируют все
полосы и заметно.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Эквалайзер на плеере больше не декоративная синус-волна — реагирует на
реальный звук. Через TeeAudioProcessor подключаемся к декодированному PCM в
аудио-конвейере ExoPlayer (без разрешений/микрофона), считаем FFT → лог-полосы
(AudioSpectrumAnalyzer), PlayerController отдаёт спектр StateFlow'ом, LiveEqualizer
рисует столбики по уровням (с быстрым ростом/плавным спадом). Когда звука нет
(пауза/float-выход) — фолбэк на прежнюю синус-волну.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Вместо простой смены — эффект переворота (как страница альбома/пластинка):
старая обложка улетает передней гранью (0–90°), новая прилетает задней
(90–180°, контр-вращение чтобы не зеркалилась). Компонент FlipCover,
подключён к обложке в плеере; срабатывает при смене coverUrl трека.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Обложки наливались общей очередью (1.5с) — играющий трек ждал свою очередь.
Добавлена приоритетная дорожка: трек, который слушают сейчас, обогащается
первым (PlayerViewModel → NowPlayingRepository.enrichCoverNow). Троттл общей
очереди ускорен 1.5с→0.8с. Дедуп разнесён на enqueued/processed, чтобы
дорожки не дублировали работу.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Серверный IP забанен Apple (iTunes search 429), а Deezer из РФ пуст — обложки
перестали наливаться. Теперь iTunes-поиск делает КЛИЕНТ (его IP не забанен):
для now-playing-треков без обложки ищет арт в iTunes и шлёт ССЫЛКУ на наш
бэкенд (POST /covers/submit), сервер качает её (CDN из РФ доступен) и кладёт
WebP — дальше обложка приходит всем через /now-playing. Дедуп по треку +
троттлинг 1.5с (CoverEnrichmentManager). Сервер: host-whitelist (SSRF),
идемпотентность (first-write-wins).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Многие станции отдают по http 301 на https; ExoPlayer по умолчанию не идёт
по кросс-протокольному редиректу и не играет их. Включён общий HTTP-источник
с setAllowCrossProtocolRedirects(true) — такие станции заиграют без правки URL.
ICY-метаданные сохраняются (обрабатываются на уровне ProgressiveMediaSource).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
royalradio.space по http отдаёт 301 на https, а ExoPlayer по умолчанию НЕ
следует кросс-протокольным редиректам (http→https) — поэтому все 10 каналов
Royal Radio не воспроизводились. Потоки переведены на прямой https (200
audio/mpeg). Прод-БД тоже обновлена (для ICY now-playing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>