Compare commits
3 Commits
91777fc459
...
feat/boots
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55a380d34b | ||
|
|
75d256eda5 | ||
| 1771a5b975 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -34,3 +34,8 @@ keystore.properties
|
||||
# Логотип: промежуточные генерации (тяжёлые), ключ — вне репо
|
||||
design/logos/gen/
|
||||
design/logos/ref_*.png
|
||||
|
||||
# Скрэтч-папка (картинки, HTML-эксперименты, мокапы RuStore) — не версионируем
|
||||
tempfiles/
|
||||
# ...кроме дизайн-файла Pencil — он остаётся под версией
|
||||
!tempfiles/radiOLA.pen
|
||||
|
||||
@@ -23,8 +23,8 @@ android {
|
||||
applicationId = "com.radiola"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 8
|
||||
versionName = "1.7"
|
||||
versionCode = 9
|
||||
versionName = "1.8"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context
|
||||
import android.media.AudioDeviceCallback
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.Uri
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
@@ -90,6 +92,31 @@ class PlayerController @Inject constructor(
|
||||
private var retryCount = 0
|
||||
private var reconnectJob: Job? = null
|
||||
|
||||
// Намерение играть: пользователь включил станцию и не ставил паузу/стоп.
|
||||
// По нему решаем, переподключаться ли после обрыва — НЕ сдаёмся навсегда.
|
||||
@Volatile private var intendedToPlay = false
|
||||
|
||||
// Слушатель сети: возврат/смена сети (Wi-Fi↔LTE в машине, выход из туннеля)
|
||||
// мгновенно переподключает поток, не дожидаясь бэк-оффа. Главная причина, по
|
||||
// которой радио раньше «не оживало» после смены сети.
|
||||
private val connectivityManager =
|
||||
context.getSystemService(ConnectivityManager::class.java)
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
if (!intendedToPlay) return
|
||||
// Колбэк — на системном потоке; ExoPlayer трогаем на main (timerScope).
|
||||
timerScope.launch {
|
||||
if (!exoPlayer.isPlaying) {
|
||||
retryCount = 0
|
||||
runCatching {
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.playWhenReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FADE_MS = 20_000L // длительность плавного затухания в конце таймера
|
||||
const val CROSSFADE_MS = 90_000L // переход радио → звук для сна (внутри аутро)
|
||||
@@ -212,7 +239,10 @@ class PlayerController @Inject constructor(
|
||||
*/
|
||||
private fun scheduleReconnect() {
|
||||
reconnectJob?.cancel()
|
||||
if (retryCount >= 10) return
|
||||
// Пока пользователь хочет играть — пробуем переподключаться бесконечно
|
||||
// (с бэк-оффом до 15с). Раньше сдавались навсегда после 10 попыток (~100с) →
|
||||
// длинный туннель/смена сети глушили радио до ручного перезапуска.
|
||||
if (!intendedToPlay) return
|
||||
val delayMs = (2_000L * (retryCount + 1)).coerceAtMost(15_000L)
|
||||
retryCount++
|
||||
reconnectJob = timerScope.launch {
|
||||
@@ -244,6 +274,7 @@ class PlayerController @Inject constructor(
|
||||
|
||||
init {
|
||||
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null)
|
||||
runCatching { connectivityManager?.registerDefaultNetworkCallback(networkCallback) }
|
||||
}
|
||||
|
||||
fun play(url: String, stationPrefix: String, stationName: String, stationId: Int? = null) {
|
||||
@@ -251,6 +282,7 @@ class PlayerController @Inject constructor(
|
||||
// Новая станция — сбрасываем переподключение предыдущего потока.
|
||||
reconnectJob?.cancel()
|
||||
retryCount = 0
|
||||
intendedToPlay = true
|
||||
_currentStationId.value = stationId
|
||||
_icyTitle.value = null
|
||||
val mediaItem = MediaItem.Builder()
|
||||
@@ -412,17 +444,19 @@ class PlayerController @Inject constructor(
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
// Пауза пользователем — отменяем отложенное переподключение, иначе оно
|
||||
// позже само возобновит воспроизведение.
|
||||
// Пауза пользователем — больше не хотим играть, отменяем переподключение.
|
||||
intendedToPlay = false
|
||||
reconnectJob?.cancel()
|
||||
exoPlayer.pause()
|
||||
}
|
||||
|
||||
fun play() {
|
||||
intendedToPlay = true
|
||||
exoPlayer.play()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
intendedToPlay = false
|
||||
reconnectJob?.cancel()
|
||||
exoPlayer.stop()
|
||||
_currentStationPrefix.value = null
|
||||
@@ -432,6 +466,7 @@ class PlayerController @Inject constructor(
|
||||
fun release() {
|
||||
timerScope.cancel()
|
||||
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
|
||||
runCatching { connectivityManager?.unregisterNetworkCallback(networkCallback) }
|
||||
sleepSoundPlayer.stop()
|
||||
exoPlayer.release()
|
||||
}
|
||||
|
||||
27
design/logos/icon_01.html
Normal file
27
design/logos/icon_01.html
Normal file
File diff suppressed because one or more lines are too long
27
design/logos/icon_02.html
Normal file
27
design/logos/icon_02.html
Normal file
File diff suppressed because one or more lines are too long
27
design/logos/icon_03.html
Normal file
27
design/logos/icon_03.html
Normal file
File diff suppressed because one or more lines are too long
27
design/logos/icon_04.html
Normal file
27
design/logos/icon_04.html
Normal file
File diff suppressed because one or more lines are too long
27
design/logos/icon_05.html
Normal file
27
design/logos/icon_05.html
Normal file
File diff suppressed because one or more lines are too long
87
design/logos/sheet.html
Normal file
87
design/logos/sheet.html
Normal file
File diff suppressed because one or more lines are too long
83
docs/rustore-listing.md
Normal file
83
docs/rustore-listing.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Карточка radiOLA для RuStore
|
||||
|
||||
Дата: 2026-06-08. Черновик контента и ассетов для публикации в RuStore.
|
||||
Связано: [дизайн релиза](superpowers/specs/2026-06-08-rustore-release-design.md), раздел F.
|
||||
|
||||
## Основное
|
||||
|
||||
| Поле | Значение |
|
||||
|---|---|
|
||||
| Название в Консоли (внутреннее, не меняется) | `radiOLA` |
|
||||
| Название для пользователей | `radiOLA — радио онлайн` |
|
||||
| Тип приложения | Универсальное |
|
||||
| Тип монетизации | Бесплатное |
|
||||
| Категория | Музыка и аудио |
|
||||
| Возрастной рейтинг | 12+ (возможна ненормативная лирика в потоках) |
|
||||
| Политика конфиденциальности | `https://api.radiola.nexaweb.su/privacy` (⏳ захостить) |
|
||||
|
||||
## Страны и регионы
|
||||
|
||||
Россия, Беларусь, Казахстан, Кыргызстан, Армения (в каталоге есть белорусские
|
||||
станции — Unistar, Новое Радио BY).
|
||||
|
||||
## Краткое описание
|
||||
|
||||
```
|
||||
Онлайн-радио: сотни станций, тексты песен, распознавание треков, запись эфира
|
||||
```
|
||||
|
||||
## Полное описание
|
||||
|
||||
```
|
||||
radiOLA — удобный плеер интернет-радио с сотнями станций и умными функциями.
|
||||
|
||||
• Сотни радиостанций — музыка, новости, разговорные
|
||||
• Что играет сейчас: трек, исполнитель и обложка в реальном времени
|
||||
• Чарты популярных треков на радио — что крутят чаще всего, с фильтром
|
||||
по жанрам и периодам, трендами роста и графиком популярности
|
||||
• Тексты песен прямо во время эфира
|
||||
• Распознавание треков (даже без метаданных станции)
|
||||
• Быстрый поиск играющего трека в Яндекс Музыке, ВК Музыке, Spotify,
|
||||
Apple Music, YouTube Music, BOOM, Tidal и Deezer — одним касанием
|
||||
• История прослушанного и распознанных песен
|
||||
• Запись эфира с перемоткой и тайм-кодами
|
||||
• Эквалайзер и улучшайзеры звука
|
||||
• Таймер сна и будильник с радио
|
||||
• Выбор качества потока, 8 цветовых тем
|
||||
• Фоновое воспроизведение, управление с локскрина
|
||||
```
|
||||
|
||||
## Ассеты (готовы)
|
||||
|
||||
Папка: `tempfiles/screensforRuStore/out/`
|
||||
|
||||
| Файл | Назначение |
|
||||
|---|---|
|
||||
| `icon_512.png` | Иконка 512×512 (тема forest, композит ic_bg/ic_fg_forest) |
|
||||
| `rustore_1.png` | Каталог станций |
|
||||
| `rustore_2.png` | Плеер + поиск трека в сервисах |
|
||||
| `rustore_3.png` | Чарты |
|
||||
| `rustore_4.png` | Тексты песен |
|
||||
| `rustore_5.png` | Записи эфира |
|
||||
| `rustore_6.png` | Эквалайзер |
|
||||
| `rustore_7.png` | Таймер сна |
|
||||
| `rustore_8.png` | Будильник / настройки |
|
||||
|
||||
Пересборка скриншотов: `tempfiles/screensforRuStore/gen.py` → `html/` → headless Chrome.
|
||||
|
||||
## Заметка модератору
|
||||
|
||||
- `SCHEDULE_EXACT_ALARM`/`USE_EXACT_ALARM` — будильник с радио (точное время срабатывания).
|
||||
- `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` — фоновое воспроизведение при выключенном
|
||||
экране / в машине. Готовы убрать по требованию модерации.
|
||||
- Приложение-агрегатор: воспроизводит публичные интернет-радиопотоки третьих лиц.
|
||||
|
||||
## Блокеры загрузки версии (из плана реализации)
|
||||
|
||||
1. HTTPS-домен `api.radiola.nexaweb.su` (DNS + Caddy).
|
||||
2. Сервер: страница `/privacy`, https в `app-version.json`.
|
||||
3. Gradle: flavors `store`/`sideload` + signingConfig release.
|
||||
4. Манифест: `REQUEST_INSTALL_PACKAGES` → sideload.
|
||||
5. Код: gate апдейтера, baseUrl → https, чистка настроек, SOVA (sideload).
|
||||
6. Keystore (генерирует пользователь).
|
||||
7. Сборка `:app:assembleStoreRelease`.
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user