feat(brand): новый 3D-логотип (монограмма R) + лого/иконка под цветовую тему

Логотип: монограмма-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).
This commit is contained in:
nk
2026-06-07 16:17:39 +03:00
parent 07f56acf27
commit 2fcc065a18
44 changed files with 617 additions and 24 deletions

View File

@@ -36,12 +36,50 @@
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Radiola.Splash"
android:configChanges="orientation|screenSize|smallestScreenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
android:configChanges="orientation|screenSize|smallestScreenSize" />
<!-- Иконка лаунчера под цветовую тему: всегда включён ровно ОДИН alias.
Переключается в рантайме (LauncherIconManager) при смене темы. -->
<activity-alias android:name=".MainAliasForest" android:enabled="true" android:exported="true"
android:targetActivity=".MainActivity" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_forest" android:roundIcon="@mipmap/ic_launcher_forest_round">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity-alias>
<activity-alias android:name=".MainAliasOcean" android:enabled="false" android:exported="true"
android:targetActivity=".MainActivity" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_ocean" android:roundIcon="@mipmap/ic_launcher_ocean_round">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity-alias>
<activity-alias android:name=".MainAliasSunset" android:enabled="false" android:exported="true"
android:targetActivity=".MainActivity" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_sunset" android:roundIcon="@mipmap/ic_launcher_sunset_round">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity-alias>
<activity-alias android:name=".MainAliasAmethyst" android:enabled="false" android:exported="true"
android:targetActivity=".MainActivity" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_amethyst" android:roundIcon="@mipmap/ic_launcher_amethyst_round">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity-alias>
<activity-alias android:name=".MainAliasNeon" android:enabled="false" android:exported="true"
android:targetActivity=".MainActivity" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_neon" android:roundIcon="@mipmap/ic_launcher_neon_round">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity-alias>
<activity-alias android:name=".MainAliasAmber" android:enabled="false" android:exported="true"
android:targetActivity=".MainActivity" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_amber" android:roundIcon="@mipmap/ic_launcher_amber_round">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity-alias>
<activity-alias android:name=".MainAliasIce" android:enabled="false" android:exported="true"
android:targetActivity=".MainActivity" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_ice" android:roundIcon="@mipmap/ic_launcher_ice_round">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity-alias>
<activity-alias android:name=".MainAliasRose" android:enabled="false" android:exported="true"
android:targetActivity=".MainActivity" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_rose" android:roundIcon="@mipmap/ic_launcher_rose_round">
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity-alias>
<service
android:name=".service.PlayerService"

View File

@@ -50,6 +50,9 @@ class MainActivity : ComponentActivity() {
@Inject
lateinit var settingsRepository: com.radiola.domain.repository.SettingsRepository
@Inject
lateinit var launcherIconManager: com.radiola.util.LauncherIconManager
// После ответа на запрос уведомлений — просим исключение из оптимизации батареи.
private val notifPermLauncher = registerForActivityResult(
androidx.activity.result.contract.ActivityResultContracts.RequestPermission()
@@ -67,6 +70,10 @@ class MainActivity : ComponentActivity() {
setContent {
// Выбранная цветовая тема (мгновенно перекрашивает всё приложение).
val paletteId by settingsRepository.getThemePalette().collectAsState(initial = "forest")
// Иконка лаунчера следует теме (срабатывает на старте и при смене темы).
LaunchedEffect(paletteId) {
launcherIconManager.applyIfNeeded(com.radiola.ui.theme.ThemePalette.fromId(paletteId))
}
RadiolaTheme(palette = com.radiola.ui.theme.ThemePalette.fromId(paletteId)) {
val navController = rememberNavController()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)

View File

@@ -23,29 +23,19 @@ import androidx.compose.material3.Text
fun brandGradient(): Brush = Brush.linearGradient(listOf(BrandGradientStart, BrandGradientEnd))
/**
* Иконка-марка приложения: градиентный squircle с монограммой «R».
* Марка приложения: объёмная 3D-монограмма «R», перекрашенная под текущую тему.
*/
@Composable
fun AppMark(
size: Dp = 76.dp,
modifier: Modifier = Modifier
) {
val radius = (size.value * 0.29f).dp
val colors = RadiolaTheme.colors
Box(
modifier = modifier
.size(size)
.clip(RoundedCornerShape(radius))
.background(colors.brandGradient),
contentAlignment = Alignment.Center
) {
Text(
text = "R",
color = colors.bgBase,
fontWeight = FontWeight.Black,
fontSize = (size.value * 0.62f).sp
)
}
val palette = RadiolaTheme.palette
androidx.compose.foundation.Image(
painter = androidx.compose.ui.res.painterResource(palette.logoRes),
contentDescription = "radiOLA",
modifier = modifier.size(size)
)
}
/**

View File

@@ -1,6 +1,7 @@
package com.radiola.ui.theme
import androidx.compose.ui.graphics.Color
import com.radiola.R
/**
* Цветовые темы приложения. radiOLA всегда тёмная — палитры различаются оттенком
@@ -112,6 +113,19 @@ enum class ThemePalette(
),
;
/** Перекрашенный под эту тему 3D-логотип (drawable). */
val logoRes: Int
get() = when (this) {
FOREST -> R.drawable.logo_forest
OCEAN -> R.drawable.logo_ocean
SUNSET -> R.drawable.logo_sunset
AMETHYST -> R.drawable.logo_amethyst
NEON -> R.drawable.logo_neon
AMBER -> R.drawable.logo_amber
ICE -> R.drawable.logo_ice
ROSE -> R.drawable.logo_rose
}
companion object {
/** Палитра по сохранённому id (фолбэк — «Лес»). */
fun fromId(id: String?): ThemePalette = entries.firstOrNull { it.id == id } ?: FOREST

View File

@@ -33,10 +33,14 @@ data class RadiolaColors(
// По умолчанию — фирменная палитра «Лес».
val LocalRadiolaColors = staticCompositionLocalOf { ThemePalette.FOREST.colors }
// Текущая палитра целиком — чтобы лого/иконка выбирали свой перекрашенный вариант.
val LocalThemePalette = staticCompositionLocalOf { ThemePalette.FOREST }
object RadiolaTheme {
val colors: RadiolaColors
@Composable get() = LocalRadiolaColors.current
val palette: ThemePalette
@Composable get() = LocalThemePalette.current
}
// Material ColorScheme из наших токенов — чтобы Material-компоненты тоже следовали палитре.
@@ -64,7 +68,10 @@ fun RadiolaTheme(
) {
// Приложение всегда тёмное; палитра выбирается пользователем.
val colors = palette.colors
CompositionLocalProvider(LocalRadiolaColors provides colors) {
CompositionLocalProvider(
LocalRadiolaColors provides colors,
LocalThemePalette provides palette
) {
MaterialTheme(
colorScheme = schemeOf(colors),
typography = Typography,

View File

@@ -0,0 +1,66 @@
package com.radiola.util
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import com.radiola.ui.theme.ThemePalette
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* Переключает иконку лаунчера под выбранную цветовую тему. Реализовано через
* activity-alias в манифесте (по одному на тему): включаем alias выбранной темы и
* выключаем остальные — ровно ОДИН активен всегда (иначе приложение пропадёт с
* рабочего стола). На части лаунчеров/ColorOS иконка обновляется с задержкой —
* это особенность системы, не баг.
*/
@Singleton
class LauncherIconManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private fun aliasSuffix(p: ThemePalette) = when (p) {
ThemePalette.FOREST -> "Forest"
ThemePalette.OCEAN -> "Ocean"
ThemePalette.SUNSET -> "Sunset"
ThemePalette.AMETHYST -> "Amethyst"
ThemePalette.NEON -> "Neon"
ThemePalette.AMBER -> "Amber"
ThemePalette.ICE -> "Ice"
ThemePalette.ROSE -> "Rose"
}
private fun component(p: ThemePalette) =
ComponentName(context.packageName, "${context.packageName}.MainAlias${aliasSuffix(p)}")
/** Меняет иконку только если она ещё не соответствует теме (без лишних миганий). */
fun applyIfNeeded(palette: ThemePalette) {
val prefs = context.getSharedPreferences("radiola_prefs", Context.MODE_PRIVATE)
// Дефолт манифеста — forest (его alias enabled=true).
if (prefs.getString("icon_alias", "forest") == palette.id) return
apply(palette)
prefs.edit().putString("icon_alias", palette.id).apply()
}
private fun apply(palette: ThemePalette) {
val pm = context.packageManager
// Сначала включаем нужный — чтобы ни на миг не остаться без launcher-иконки.
runCatching {
pm.setComponentEnabledSetting(
component(palette),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
}
for (p in ThemePalette.entries) {
if (p == palette) continue
runCatching {
pm.setComponentEnabledSetting(
component(p),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_amber"/>
<foreground android:drawable="@drawable/ic_fg_amber"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_amber"/>
<foreground android:drawable="@drawable/ic_fg_amber"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_amethyst"/>
<foreground android:drawable="@drawable/ic_fg_amethyst"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_amethyst"/>
<foreground android:drawable="@drawable/ic_fg_amethyst"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_forest"/>
<foreground android:drawable="@drawable/ic_fg_forest"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_forest"/>
<foreground android:drawable="@drawable/ic_fg_forest"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_ice"/>
<foreground android:drawable="@drawable/ic_fg_ice"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_ice"/>
<foreground android:drawable="@drawable/ic_fg_ice"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_neon"/>
<foreground android:drawable="@drawable/ic_fg_neon"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_neon"/>
<foreground android:drawable="@drawable/ic_fg_neon"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_ocean"/>
<foreground android:drawable="@drawable/ic_fg_ocean"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_ocean"/>
<foreground android:drawable="@drawable/ic_fg_ocean"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_rose"/>
<foreground android:drawable="@drawable/ic_fg_rose"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_rose"/>
<foreground android:drawable="@drawable/ic_fg_rose"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_sunset"/>
<foreground android:drawable="@drawable/ic_fg_sunset"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_bg_sunset"/>
<foreground android:drawable="@drawable/ic_fg_sunset"/>
</adaptive-icon>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_bg_forest">#0C1410</color>
<color name="ic_bg_ocean">#0A0F1A</color>
<color name="ic_bg_sunset">#1A0F0C</color>
<color name="ic_bg_amethyst">#120E1A</color>
<color name="ic_bg_neon">#0D0A12</color>
<color name="ic_bg_amber">#14110A</color>
<color name="ic_bg_ice">#0C1014</color>
<color name="ic_bg_rose">#160E12</color>
</resources>