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).
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
66
app/src/main/java/com/radiola/util/LauncherIconManager.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-nodpi/ic_fg_amber.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_amethyst.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_forest.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_ice.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_neon.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_ocean.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_rose.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_sunset.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_amber.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_amethyst.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_forest.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_ice.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_neon.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_ocean.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_rose.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_sunset.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_ice.xml
Normal 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>
|
||||
@@ -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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon.xml
Normal 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>
|
||||
@@ -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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean.xml
Normal 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>
|
||||
@@ -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>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_rose.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
11
app/src/main/res/values/ic_colors.xml
Normal 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>
|
||||