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:
@@ -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
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user