From 94d2ffa49e5748bfdabf5b969c282cfa4d224a8f Mon Sep 17 00:00:00 2001 From: papi <20916260+papi-ux@users.noreply.github.com> Date: Sat, 27 Jun 2026 02:06:04 -0400 Subject: [PATCH 1/3] feat(ui): add themed glass drawer chrome Isolates Nova Android theme/drawer work on a feature branch from origin/master. Adds shared NovaSheetChrome glass surfaces, PSP Portable Chrome theme picker/focus polish, and source guards for themed sheet/focus resources. --- .gitignore | 1 + app/src/main/java/com/papi/nova/AppView.kt | 14 +- app/src/main/java/com/papi/nova/PcView.kt | 283 ++++++++++++++++-- .../com/papi/nova/ui/NovaGameDetailSheet.kt | 74 +---- .../com/papi/nova/ui/NovaLibraryActivity.kt | 7 +- .../com/papi/nova/ui/NovaPolarisSyncSheet.kt | 53 +--- .../java/com/papi/nova/ui/NovaSheetChrome.kt | 199 ++++++++++++ .../java/com/papi/nova/ui/NovaThemeManager.kt | 42 +-- .../papi/nova/ui/compose/NovaComposeTheme.kt | 50 ++-- .../main/res/color/nova_chip_bg_selector.xml | 2 +- .../res/color/nova_focus_stroke_selector.xml | 2 +- .../main/res/drawable/nova_chip_selected.xml | 2 +- .../res/drawable/nova_featured_action_bg.xml | 2 +- .../drawable/nova_server_row_focus_ring.xml | 2 +- .../main/res/layout-land/activity_pc_view.xml | 3 +- app/src/main/res/layout/activity_pc_view.xml | 3 +- .../res/layout/nova_app_context_sheet.xml | 4 +- app/src/main/res/values/arrays.xml | 4 +- app/src/main/res/values/colors_nova.xml | 55 ++-- app/src/main/res/values/strings.xml | 12 +- app/src/main/res/values/styles.xml | 76 ++--- .../com/papi/nova/ui/NovaFocusDrawableTest.kt | 20 +- .../com/papi/nova/ui/NovaThemeManagerTest.kt | 59 ++-- .../papi/nova/ui/NovaThemeResourcesTest.kt | 180 ++++++++++- 24 files changed, 832 insertions(+), 317 deletions(-) create mode 100644 app/src/main/java/com/papi/nova/ui/NovaSheetChrome.kt diff --git a/.gitignore b/.gitignore index c954d730..9b912162 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ app/.externalNativeBuild/ .cxx/ # agent files +.hermes/ AGENTS.md CLAUDE.md .githooks/ diff --git a/app/src/main/java/com/papi/nova/AppView.kt b/app/src/main/java/com/papi/nova/AppView.kt index b3de744e..f72583c1 100644 --- a/app/src/main/java/com/papi/nova/AppView.kt +++ b/app/src/main/java/com/papi/nova/AppView.kt @@ -44,6 +44,7 @@ import com.papi.nova.profiles.ProfilesManager import com.papi.nova.runtime.NovaRuntimeTasks import com.papi.nova.ui.AdapterFragment import com.papi.nova.ui.AdapterFragmentCallbacks +import com.papi.nova.ui.NovaSheetChrome import com.papi.nova.ui.NovaThemeManager import com.papi.nova.utils.CacheHelper import com.papi.nova.utils.Dialog @@ -732,8 +733,11 @@ class AppView : AppCompatActivity(), AdapterFragmentCallbacks { private fun showAppBottomSheet(selectedApp: AppObject) { val sheet = BottomSheetDialog(this, R.style.NovaBottomSheet) sheet.setContentView(R.layout.nova_app_context_sheet) - sheet.behavior.state = BottomSheetBehavior.STATE_EXPANDED - sheet.behavior.skipCollapsed = true + val sheetRoot = sheet.findViewById(R.id.nova_sheet_root) + sheet.setOnShowListener { + NovaSheetChrome.applyBottomSheetChrome(sheet, sheetRoot) + sheet.findViewById(R.id.sheet_app_name)?.let(NovaSheetChrome::styleSheetTitle) + } val titleView = sheet.findViewById(R.id.sheet_app_name) titleView?.text = selectedApp.app.appName @@ -869,16 +873,12 @@ class AppView : AppCompatActivity(), AdapterFragmentCallbacks { val item = TextView(this) item.text = label item.textSize = 15f - item.setTextColor(ContextCompat.getColor(this, R.color.nova_text_primary)) + NovaSheetChrome.styleSheetAction(item) item.typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.NORMAL) val pad = UiHelper.dpToPx(this, 24f).toInt() val padV = UiHelper.dpToPx(this, 14f).toInt() item.setPadding(pad, padV, pad, padV) - val outValue = android.util.TypedValue() - theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true) - item.setBackgroundResource(outValue.resourceId) - item.setOnClickListener { action.run() } container.addView(item) } diff --git a/app/src/main/java/com/papi/nova/PcView.kt b/app/src/main/java/com/papi/nova/PcView.kt index 259369fa..5b9dcac1 100644 --- a/app/src/main/java/com/papi/nova/PcView.kt +++ b/app/src/main/java/com/papi/nova/PcView.kt @@ -8,6 +8,8 @@ import android.content.Intent import android.content.ServiceConnection import android.content.res.ColorStateList import android.content.res.Configuration +import android.graphics.Typeface +import android.graphics.drawable.GradientDrawable import android.net.Uri import android.opengl.GLSurfaceView import android.os.Build @@ -18,6 +20,7 @@ import android.os.Looper import android.os.SystemClock import android.text.InputFilter import android.text.InputType +import android.view.Gravity import android.view.KeyEvent import android.view.View import android.view.ViewGroup @@ -67,6 +70,7 @@ import com.papi.nova.ui.AdapterFragment import com.papi.nova.ui.AdapterFragmentCallbacks import com.papi.nova.ui.NovaLibraryActivity import com.papi.nova.ui.NovaQrScanActivity +import com.papi.nova.ui.NovaSheetChrome import com.papi.nova.ui.NovaSnackbar import com.papi.nova.ui.NovaThemeManager import com.papi.nova.ui.NovaWelcomeActivity @@ -87,6 +91,7 @@ import javax.microedition.khronos.opengles.GL10 import org.xmlpull.v1.XmlPullParserException class PcView : AppCompatActivity(), AdapterFragmentCallbacks { + private val THEME_PICKER_GRID_GAP_DP = 12 private var noPcFoundLayout: View? = null private lateinit var pcGridAdapter: PcGridAdapter private lateinit var shortcutHelper: ShortcutHelper @@ -433,8 +438,6 @@ class PcView : AppCompatActivity(), AdapterFragmentCallbacks { swipeRefresh.setColorSchemeColors(accent) swipeRefresh.setProgressBackgroundColorSchemeColor(surface) } - findViewById(R.id.pcs_loading)?.indeterminateTintList = - ColorStateList.valueOf(accent) findViewById(R.id.pcViewTitle)?.setTextColor(textPrimary) findViewById(R.id.pcViewSectionLabel)?.setTextColor(textMuted) @@ -541,8 +544,99 @@ class PcView : AppCompatActivity(), AdapterFragmentCallbacks { } private fun showThemePicker(anchor: View?) { + val themes = buildThemePickerThemes() + val currentTheme = NovaThemeManager.getTheme(this) + val surface = NovaThemeManager.getCardBackgroundColor(this) + val textPrimary = NovaThemeManager.getTextPrimaryColor(this) + val textSecondary = NovaThemeManager.getTextSecondaryColor(this) + val textMuted = NovaThemeManager.getTextMutedColor(this) + + val dialog = BottomSheetDialog(this, R.style.NovaBottomSheet) + var focusTarget: View? = null + lateinit var themePickerFocusLabel: TextView + + val content = NovaSheetChrome.createSheetContainer(this) + + content.addView( + TextView(this).apply { + text = getString(R.string.pcview_theme_picker_title) + setTextColor(textPrimary) + textSize = 22f + typeface = Typeface.DEFAULT_BOLD + includeFontPadding = false + }, + ) + content.addView( + TextView(this).apply { + text = getString(R.string.pcview_theme_picker_hint) + setTextColor(textMuted) + textSize = 12f + setPadding(0, dp(6), 0, dp(14)) + }, + ) + + themePickerFocusLabel = TextView(this).apply { + text = getString( + R.string.pcview_theme_picker_focus_format, + NovaThemeManager.getThemeLabel(this@PcView, currentTheme), + getThemePickerSubtitle(currentTheme), + ) + setTextColor(NovaThemeManager.getAccentColor(this@PcView)) + textSize = 12f + typeface = Typeface.DEFAULT_BOLD + setPadding(0, 0, 0, dp(12)) + } + content.addView(themePickerFocusLabel) + + val themeGrid = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + val gridGap = dp(THEME_PICKER_GRID_GAP_DP) + setPadding(gridGap, 0, gridGap, gridGap) + } + content.addView(themeGrid) + themes.chunked(2).forEach { themePair -> + val gridRow = LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + } + themePair.forEachIndexed { index, theme -> + val row = createThemePickerRow(theme, currentTheme, themePickerFocusLabel, surface, textPrimary, textSecondary, dialog) + row.layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply { + if (index == 0) { + marginEnd = dp(THEME_PICKER_GRID_GAP_DP) + } + bottomMargin = dp(THEME_PICKER_GRID_GAP_DP) + } + if (focusTarget == null || theme == currentTheme) { + focusTarget = row + } + gridRow.addView(row) + } + if (themePair.size == 1) { + gridRow.addView( + View(this).apply { + layoutParams = LinearLayout.LayoutParams(0, 1, 1f) + }, + ) + } + themeGrid.addView(gridRow) + } + + dialog.setContentView(content) + dialog.setOnShowListener { + NovaSheetChrome.applyBottomSheetChrome(dialog, content) + content.post { + focusTarget?.requestFocus() + } + } + dialog.show() + anchor?.performHapticFeedback(android.view.HapticFeedbackConstants.CONFIRM) + } + + private fun buildThemePickerThemes(): List { val themes = mutableListOf( NovaThemeManager.THEME_POLARIS, + NovaThemeManager.THEME_PORTABLE_CHROME, NovaThemeManager.THEME_OLED, NovaThemeManager.THEME_MIAMI, NovaThemeManager.THEME_HIGH_CONTRAST, @@ -550,20 +644,174 @@ class PcView : AppCompatActivity(), AdapterFragmentCallbacks { if (NovaThemeManager.isMaterialYouAvailable()) { themes.add(NovaThemeManager.THEME_MATERIAL_YOU) } - val labels = themes.map { NovaThemeManager.getThemeLabel(this, it) }.toTypedArray() - val currentTheme = NovaThemeManager.getTheme(this) - val checkedIndex = themes.indexOf(currentTheme).coerceAtLeast(0) + return themes + } - AlertDialog.Builder(this) - .setTitle(R.string.pcview_theme_picker_title) - .setSingleChoiceItems(labels, checkedIndex) { dialog, which -> - applyThemeSelection(themes[which]) + private fun createThemePickerRow( + theme: String, + currentTheme: String, + themePickerFocusLabel: TextView, + surface: Int, + textPrimary: Int, + textSecondary: Int, + dialog: BottomSheetDialog, + ): MaterialCardView { + val label = NovaThemeManager.getThemeLabel(this, theme) + val subtitle = getThemePickerSubtitle(theme) + val rowAccent = getThemePickerPreviewAccent(theme) + val divider = NovaThemeManager.getDividerColor(this) + val selected = theme == currentTheme + + val card = MaterialCardView(this).apply { + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + ).apply { + bottomMargin = dp(10) + } + radius = dp(18).toFloat() + isClickable = true + isFocusable = true + setOnClickListener { dialog.dismiss() + applyThemeSelection(theme) } - .show() - anchor?.performHapticFeedback(android.view.HapticFeedbackConstants.CONFIRM) + setOnKeyListener { _, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && + (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_BUTTON_A) + ) { + performClick() + true + } else { + false + } + } + } + + val row = LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + setPadding(dp(16), dp(14), dp(16), dp(14)) + } + row.addView( + View(this).apply { + background = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(rowAccent) + setStroke(dp(2), ColorUtils.blendARGB(rowAccent, textPrimary, 0.32f)) + } + layoutParams = LinearLayout.LayoutParams(dp(18), dp(18)).apply { + marginEnd = dp(14) + } + }, + ) + row.addView( + LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + addView( + TextView(this@PcView).apply { + text = label + setTextColor(textPrimary) + textSize = 17f + typeface = Typeface.DEFAULT_BOLD + includeFontPadding = false + }, + ) + addView( + TextView(this@PcView).apply { + text = subtitle + setTextColor(textSecondary) + textSize = 12f + setPadding(0, dp(5), dp(8), 0) + }, + ) + }, + ) + if (selected) { + row.addView( + TextView(this).apply { + text = getString(R.string.pcview_theme_picker_current_badge) + setTextColor(textPrimary) + textSize = 11f + typeface = Typeface.DEFAULT_BOLD + gravity = Gravity.CENTER + includeFontPadding = false + background = GradientDrawable().apply { + setColor(ColorUtils.blendARGB(surface, rowAccent, 0.30f)) + setStroke(dp(1), rowAccent) + cornerRadius = dp(999).toFloat() + } + setPadding(dp(12), dp(7), dp(12), dp(7)) + }, + ) + } + card.addView(row) + updateThemePickerRowState(card, selected, false, rowAccent, surface, divider, themePickerFocusLabel, label, subtitle) + card.setOnFocusChangeListener { _, hasFocus -> + updateThemePickerRowState(card, selected, hasFocus, rowAccent, surface, divider, themePickerFocusLabel, label, subtitle) + } + return card } + private fun updateThemePickerRowState( + card: MaterialCardView, + selected: Boolean, + focused: Boolean, + rowAccent: Int, + surface: Int, + divider: Int, + themePickerFocusLabel: TextView, + label: String, + subtitle: String, + ) { + card.setCardBackgroundColor( + when { + focused -> ColorUtils.blendARGB(surface, rowAccent, 0.18f) + selected -> ColorUtils.blendARGB(surface, rowAccent, 0.14f) + else -> surface + }, + ) + card.strokeColor = if (focused || selected) rowAccent else divider + card.strokeWidth = dp( + when { + focused -> 4 + selected -> 3 + else -> 1 + }, + ) + if (focused) { + themePickerFocusLabel.text = getString(R.string.pcview_theme_picker_focus_format, label, subtitle) + themePickerFocusLabel.setTextColor(rowAccent) + } + } + + private fun getThemePickerSubtitle(theme: String): String { + return when (theme) { + NovaThemeManager.THEME_PORTABLE_CHROME -> getString(R.string.pcview_theme_portable_chrome_subtitle) + NovaThemeManager.THEME_OLED -> getString(R.string.pcview_theme_oled_subtitle) + NovaThemeManager.THEME_MIAMI -> getString(R.string.pcview_theme_miami_subtitle) + NovaThemeManager.THEME_HIGH_CONTRAST -> getString(R.string.pcview_theme_high_contrast_subtitle) + NovaThemeManager.THEME_MATERIAL_YOU -> getString(R.string.pcview_theme_material_you_subtitle) + else -> getString(R.string.pcview_theme_polaris_subtitle) + } + } + + private fun getThemePickerPreviewAccent(theme: String): Int { + return ContextCompat.getColor( + this, + when (theme) { + NovaThemeManager.THEME_PORTABLE_CHROME -> R.color.nova_portable_accent + NovaThemeManager.THEME_OLED -> R.color.nova_oled_accent + NovaThemeManager.THEME_MIAMI -> R.color.nova_miami_accent + NovaThemeManager.THEME_HIGH_CONTRAST -> R.color.nova_hc_accent + else -> R.color.nova_polaris_accent + }, + ) + } + + private fun dp(value: Int): Int = UiHelper.dpToPx(this, value.toFloat()).toInt() + private fun applyThemeSelection(theme: String) { if (theme == NovaThemeManager.getTheme(this)) { return @@ -1198,8 +1446,11 @@ class PcView : AppCompatActivity(), AdapterFragmentCallbacks { val sheet = BottomSheetDialog(this, R.style.NovaBottomSheet) sheet.setContentView(R.layout.nova_app_context_sheet) - sheet.behavior.state = BottomSheetBehavior.STATE_EXPANDED - sheet.behavior.skipCollapsed = true + val sheetRoot = sheet.findViewById(R.id.nova_sheet_root) + sheet.setOnShowListener { + NovaSheetChrome.applyBottomSheetChrome(sheet, sheetRoot) + sheet.findViewById(R.id.sheet_app_name)?.let(NovaSheetChrome::styleSheetTitle) + } sheet.setOnDismissListener { startComputerUpdates() } val titleView = sheet.findViewById(R.id.sheet_app_name) @@ -1329,11 +1580,10 @@ class PcView : AppCompatActivity(), AdapterFragmentCallbacks { val deleteItem = TextView(this) deleteItem.text = getString(R.string.pcview_menu_delete_pc) deleteItem.textSize = 15f - deleteItem.setTextColor(ContextCompat.getColor(this, R.color.nova_error)) + NovaSheetChrome.styleSheetAction(deleteItem, destructive = true) val pad = UiHelper.dpToPx(this, 24f).toInt() val padV = UiHelper.dpToPx(this, 14f).toInt() deleteItem.setPadding(pad, padV, pad, padV) - deleteItem.setBackgroundResource(R.drawable.nova_dialog_choice_bg) UiHelper.applyTvFocusStyle(deleteItem) deleteItem.setOnClickListener { sheet.dismiss() @@ -1378,11 +1628,10 @@ class PcView : AppCompatActivity(), AdapterFragmentCallbacks { val item = TextView(this) item.text = label item.textSize = 15f - item.setTextColor(ContextCompat.getColor(this, R.color.nova_text_primary)) + NovaSheetChrome.styleSheetAction(item) val pad = UiHelper.dpToPx(this, 24f).toInt() val padV = UiHelper.dpToPx(this, 14f).toInt() item.setPadding(pad, padV, pad, padV) - item.setBackgroundResource(R.drawable.nova_dialog_choice_bg) UiHelper.applyTvFocusStyle(item) item.setOnClickListener { action.run() } container.addView(item) diff --git a/app/src/main/java/com/papi/nova/ui/NovaGameDetailSheet.kt b/app/src/main/java/com/papi/nova/ui/NovaGameDetailSheet.kt index 99a7594c..fe2ccffd 100644 --- a/app/src/main/java/com/papi/nova/ui/NovaGameDetailSheet.kt +++ b/app/src/main/java/com/papi/nova/ui/NovaGameDetailSheet.kt @@ -2,14 +2,12 @@ package com.papi.nova.ui import android.app.Dialog import android.content.Context -import android.content.res.Configuration import android.os.Bundle import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView -import android.widget.ScrollView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.compose.foundation.background @@ -55,9 +53,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat -import androidx.core.widget.NestedScrollView import androidx.lifecycle.lifecycleScope -import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.papi.nova.LimeLog @@ -117,7 +113,7 @@ class NovaGameDetailSheet : BottomSheetDialogFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setBackgroundResource(sheetBackgroundRes()) + background = NovaSheetChrome.createSheetBackground(requireContext()) } } @@ -820,65 +816,14 @@ class NovaGameDetailSheet : BottomSheetDialogFragment() { ).filterNotNull().joinToString(" · ") } - private fun sheetBackgroundRes(): Int { - return if (NovaThemeManager.isOled(requireContext())) { - R.drawable.nova_sheet_bg_oled - } else { - R.drawable.nova_sheet_bg - } - } - private fun expandBottomSheet(bottomSheetDialog: BottomSheetDialog?) { - val sheet = bottomSheetDialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet) ?: return + bottomSheetDialog ?: return val contentView = view ?: return - sheet.setBackgroundResource(sheetBackgroundRes()) - contentView.post { - val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - val maxHeightRatio = if (isLandscape) 0.96f else 0.90f - val maxHeight = (resources.displayMetrics.heightPixels * maxHeightRatio).toInt() - val contentHeight = contentView.measuredHeight.takeIf { it > 0 } ?: return@post - val desiredHeight = contentHeight.coerceAtMost(maxHeight) - val displayWidth = resources.displayMetrics.widthPixels - val density = resources.displayMetrics.density - val desiredWidth = if (isLandscape) { - val minWidth = (720 * density).toInt() - val maxWidth = (1260 * density).toInt() - (displayWidth * 0.7f).toInt().coerceIn(minWidth, maxWidth) - } else { - displayWidth - } - val horizontalMargin = if (isLandscape) { - ((displayWidth - desiredWidth) / 2).coerceAtLeast((18 * density).toInt()) - } else { - 0 - } - - contentView.layoutParams = contentView.layoutParams.apply { - height = if (contentHeight > maxHeight) desiredHeight else ViewGroup.LayoutParams.WRAP_CONTENT - } - sheet.layoutParams = sheet.layoutParams.apply { - width = if (isLandscape) displayWidth - (horizontalMargin * 2) else ViewGroup.LayoutParams.MATCH_PARENT - height = desiredHeight - } - (sheet.layoutParams as? ViewGroup.MarginLayoutParams)?.let { lp -> - lp.marginStart = horizontalMargin - lp.marginEnd = horizontalMargin - sheet.layoutParams = lp - } - sheet.minimumHeight = 0 - sheet.requestLayout() - - val behavior = BottomSheetBehavior.from(sheet) - behavior.isFitToContents = true - behavior.skipCollapsed = true - behavior.peekHeight = desiredHeight - behavior.state = BottomSheetBehavior.STATE_EXPANDED - - when (contentView) { - is NestedScrollView -> contentView.post { contentView.scrollTo(0, 0) } - is ScrollView -> contentView.post { contentView.scrollTo(0, 0) } - } - } + NovaSheetChrome.applyBottomSheetChrome(bottomSheetDialog, + contentView, + minLandscapeWidthDp = 720, + maxLandscapeWidthDp = 1260 + ) } } @@ -939,7 +884,10 @@ fun NovaGameDetailSheetContent( Column( modifier = modifier .fillMaxWidth() - .clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)) + .clip(RoundedCornerShape( + topStart = NovaSheetChrome.SHEET_CORNER_RADIUS_DP.dp, + topEnd = NovaSheetChrome.SHEET_CORNER_RADIUS_DP.dp + )) .background(surfaces.panel) .verticalScroll(verticalScroll) .padding(bottom = 16.dp) diff --git a/app/src/main/java/com/papi/nova/ui/NovaLibraryActivity.kt b/app/src/main/java/com/papi/nova/ui/NovaLibraryActivity.kt index d9990737..19532d36 100644 --- a/app/src/main/java/com/papi/nova/ui/NovaLibraryActivity.kt +++ b/app/src/main/java/com/papi/nova/ui/NovaLibraryActivity.kt @@ -3248,10 +3248,13 @@ class NovaLibraryActivity : AppCompatActivity() { ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, - shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), + shape = RoundedCornerShape( + topStart = NovaSheetChrome.SHEET_CORNER_RADIUS_DP.dp, + topEnd = NovaSheetChrome.SHEET_CORNER_RADIUS_DP.dp + ), containerColor = surfaces.panel, contentColor = colors.textPrimary, - scrimColor = surfaces.backgroundScrim.copy(alpha = 0.30f) + scrimColor = surfaces.backgroundScrim.copy(alpha = NovaSheetChrome.SCRIM_ALPHA) ) { Column( modifier = Modifier diff --git a/app/src/main/java/com/papi/nova/ui/NovaPolarisSyncSheet.kt b/app/src/main/java/com/papi/nova/ui/NovaPolarisSyncSheet.kt index 1b91aaf0..47772b99 100644 --- a/app/src/main/java/com/papi/nova/ui/NovaPolarisSyncSheet.kt +++ b/app/src/main/java/com/papi/nova/ui/NovaPolarisSyncSheet.kt @@ -1,7 +1,6 @@ package com.papi.nova.ui import android.app.Dialog -import android.content.res.Configuration import android.os.Bundle import android.os.SystemClock import android.view.LayoutInflater @@ -16,7 +15,6 @@ import androidx.compose.runtime.key import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.lifecycle.lifecycleScope -import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.papi.nova.LimeLog @@ -326,52 +324,13 @@ class NovaPolarisSyncSheet : BottomSheetDialogFragment() { } private fun expandBottomSheet(bottomSheetDialog: BottomSheetDialog?) { - val sheet = bottomSheetDialog?.findViewById(com.google.android.material.R.id.design_bottom_sheet) ?: return + bottomSheetDialog ?: return val contentView = view ?: return - sheet.setBackgroundResource( - if (NovaThemeManager.isOled(requireContext())) { - R.drawable.nova_sheet_bg_oled - } else { - R.drawable.nova_sheet_bg - } + NovaSheetChrome.applyBottomSheetChrome(bottomSheetDialog, + contentView, + widthFraction = 0.62f, + minLandscapeWidthDp = 700, + maxLandscapeWidthDp = 980 ) - contentView.post { - val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - val maxHeightRatio = if (isLandscape) 0.96f else 0.90f - val maxHeight = (resources.displayMetrics.heightPixels * maxHeightRatio).toInt() - val contentHeight = contentView.measuredHeight.takeIf { it > 0 } ?: return@post - val desiredHeight = contentHeight.coerceAtMost(maxHeight) - val displayWidth = resources.displayMetrics.widthPixels - val density = resources.displayMetrics.density - val desiredWidth = if (isLandscape) { - val minWidth = (700 * density).toInt() - val maxWidth = (980 * density).toInt() - (displayWidth * 0.62f).toInt().coerceIn(minWidth, maxWidth) - } else { - displayWidth - } - val horizontalMargin = if (isLandscape) { - ((displayWidth - desiredWidth) / 2).coerceAtLeast((18 * density).toInt()) - } else { - 0 - } - - sheet.layoutParams = sheet.layoutParams.apply { - width = if (isLandscape) desiredWidth else ViewGroup.LayoutParams.MATCH_PARENT - height = desiredHeight - } - (sheet.layoutParams as? ViewGroup.MarginLayoutParams)?.let { lp -> - lp.marginStart = horizontalMargin - lp.marginEnd = horizontalMargin - sheet.layoutParams = lp - } - sheet.setPadding(0, 0, 0, 0) - sheet.requestLayout() - BottomSheetBehavior.from(sheet).apply { - peekHeight = desiredHeight - state = BottomSheetBehavior.STATE_EXPANDED - skipCollapsed = true - } - } } } diff --git a/app/src/main/java/com/papi/nova/ui/NovaSheetChrome.kt b/app/src/main/java/com/papi/nova/ui/NovaSheetChrome.kt new file mode 100644 index 00000000..d0bac5f7 --- /dev/null +++ b/app/src/main/java/com/papi/nova/ui/NovaSheetChrome.kt @@ -0,0 +1,199 @@ +package com.papi.nova.ui + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.TextView +import androidx.core.graphics.ColorUtils +import androidx.core.view.setPadding +import androidx.core.widget.NestedScrollView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.papi.nova.R + +/** + * Shared Nova bottom-sheet chrome. + * + * Keep drawer/sheet surfaces here so theme palettes stay distinct while sheet + * geometry, scrim, radius, stroke, and D-pad action rows feel like one app. + */ +object NovaSheetChrome { + const val SHEET_CORNER_RADIUS_DP = 26 + const val LANDSCAPE_WIDTH_FRACTION = 0.70f + const val SCRIM_ALPHA = 0.22f + + /** Default glass opacity for NovaHUD-friendly drawer overlays. */ + const val SHEET_GLASS_ALPHA = 0.62f + const val PORTABLE_CHROME_SHEET_GLASS_ALPHA = 0.58f + const val MIAMI_SHEET_GLASS_ALPHA = 0.64f + const val OLED_SHEET_GLASS_ALPHA = 0.70f + const val MATERIAL_YOU_SHEET_GLASS_ALPHA = 0.60f + const val HIGH_CONTRAST_SHEET_GLASS_ALPHA = 0.94f + + fun createSheetContainer( + context: Context, + horizontalPaddingDp: Int = 22, + topPaddingDp: Int = 18, + bottomPaddingDp: Int = 26 + ): LinearLayout { + return LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding( + dp(context, horizontalPaddingDp), + dp(context, topPaddingDp), + dp(context, horizontalPaddingDp), + dp(context, bottomPaddingDp) + ) + background = createSheetBackground(context) + clipToOutline = true + } + } + + fun applyBottomSheetChrome( + dialog: BottomSheetDialog, + contentView: View? = null, + widthFraction: Float = LANDSCAPE_WIDTH_FRACTION, + minLandscapeWidthDp: Int = 660, + maxLandscapeWidthDp: Int = 1120, + maxHeightLandscape: Float = 0.94f, + maxHeightPortrait: Float = 0.90f + ) { + val context = dialog.context + dialog.window?.let { window -> + window.setDimAmount(SCRIM_ALPHA) + window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + } + + val sheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) ?: return + sheet.background = createSheetBackground(context) + sheet.clipToOutline = true + sheet.setPadding(0, 0, 0, 0) + contentView?.clipToOutline = true + + val measuredView = contentView ?: sheet + measuredView.post { + val resources = context.resources + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val density = resources.displayMetrics.density + val displayWidth = resources.displayMetrics.widthPixels + val displayHeight = resources.displayMetrics.heightPixels + val maxHeight = (displayHeight * if (isLandscape) maxHeightLandscape else maxHeightPortrait).toInt() + val measuredHeight = measuredView.measuredHeight.takeIf { it > 0 } ?: sheet.measuredHeight + val desiredHeight = measuredHeight.takeIf { it > 0 }?.coerceAtMost(maxHeight) ?: maxHeight + val minWidth = dp(context, minLandscapeWidthDp) + val maxWidth = dp(context, maxLandscapeWidthDp) + val landscapeWidth = (displayWidth * widthFraction).toInt() + .coerceIn(minWidth.coerceAtMost(displayWidth), maxWidth.coerceAtMost(displayWidth)) + .coerceAtMost(displayWidth - dp(context, 36)) + val desiredWidth = if (isLandscape) landscapeWidth else ViewGroup.LayoutParams.MATCH_PARENT + val horizontalMargin = if (isLandscape) { + ((displayWidth - landscapeWidth) / 2).coerceAtLeast(dp(context, 18)) + } else { + 0 + } + + contentView?.layoutParams = contentView.layoutParams?.apply { + height = if (measuredHeight > maxHeight) desiredHeight else ViewGroup.LayoutParams.WRAP_CONTENT + } + sheet.layoutParams = sheet.layoutParams.apply { + width = desiredWidth + height = if (measuredHeight > maxHeight) desiredHeight else ViewGroup.LayoutParams.WRAP_CONTENT + } + (sheet.layoutParams as? ViewGroup.MarginLayoutParams)?.let { lp -> + lp.marginStart = horizontalMargin + lp.marginEnd = horizontalMargin + sheet.layoutParams = lp + } + sheet.minimumHeight = 0 + sheet.requestLayout() + + BottomSheetBehavior.from(sheet).apply { + isFitToContents = true + skipCollapsed = true + peekHeight = desiredHeight + state = BottomSheetBehavior.STATE_EXPANDED + } + + when (contentView) { + is NestedScrollView -> contentView.post { contentView.scrollTo(0, 0) } + is ScrollView -> contentView.post { contentView.scrollTo(0, 0) } + } + } + } + + fun styleSheetTitle(title: TextView) { + title.setTextColor(NovaThemeManager.getTextPrimaryColor(title.context)) + } + + fun styleSheetAction(action: TextView, destructive: Boolean = false) { + val context = action.context + action.setTextColor( + if (destructive) context.getColor(R.color.nova_error) else NovaThemeManager.getTextPrimaryColor(context) + ) + action.background = createActionBackground(context) + action.isClickable = true + action.isFocusable = true + } + + fun createSheetBackground(context: Context): GradientDrawable { + val radius = dp(context, SHEET_CORNER_RADIUS_DP).toFloat() + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + setColor(createSheetSurfaceColor(context)) + cornerRadii = floatArrayOf(radius, radius, radius, radius, 0f, 0f, 0f, 0f) + setStroke(dp(context, 1), getSheetStrokeColor(context)) + } + } + + fun createSheetSurfaceColor(context: Context): Int { + val baseSurface = NovaThemeManager.getDialogBackgroundColor(context) + val alpha = (getSheetGlassAlpha(context) * 255).toInt().coerceIn(0, 255) + return ColorUtils.setAlphaComponent(baseSurface, alpha) + } + + fun getSheetGlassAlpha(context: Context): Float { + return when { + NovaThemeManager.isHighContrast(context) -> HIGH_CONTRAST_SHEET_GLASS_ALPHA + NovaThemeManager.isPortableChrome(context) -> PORTABLE_CHROME_SHEET_GLASS_ALPHA + NovaThemeManager.isMiami(context) -> MIAMI_SHEET_GLASS_ALPHA + NovaThemeManager.isOled(context) -> OLED_SHEET_GLASS_ALPHA + NovaThemeManager.isMaterialYou(context) -> MATERIAL_YOU_SHEET_GLASS_ALPHA + else -> SHEET_GLASS_ALPHA + } + } + + fun getSheetStrokeColor(context: Context): Int { + val surface = NovaThemeManager.getDialogBackgroundColor(context) + val accent = NovaThemeManager.getAccentColor(context) + return when { + NovaThemeManager.isHighContrast(context) -> NovaThemeManager.getDividerColor(context) + NovaThemeManager.isPortableChrome(context) -> ColorUtils.blendARGB(surface, accent, 0.46f) + NovaThemeManager.isOled(context) -> ColorUtils.blendARGB(surface, Color.WHITE, 0.12f) + else -> ColorUtils.blendARGB(surface, accent, 0.32f) + } + } + + private fun createActionBackground(context: Context): GradientDrawable { + val radius = dp(context, 16).toFloat() + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + setColor(Color.TRANSPARENT) + cornerRadius = radius + setStroke(dp(context, 1), ColorUtils.blendARGB( + NovaThemeManager.getDialogBackgroundColor(context), + NovaThemeManager.getAccentColor(context), + 0.18f + )) + } + } + + private fun dp(context: Context, value: Int): Int { + return (value * context.resources.displayMetrics.density).toInt() + } +} diff --git a/app/src/main/java/com/papi/nova/ui/NovaThemeManager.kt b/app/src/main/java/com/papi/nova/ui/NovaThemeManager.kt index 8c5f22e6..0237dbbf 100644 --- a/app/src/main/java/com/papi/nova/ui/NovaThemeManager.kt +++ b/app/src/main/java/com/papi/nova/ui/NovaThemeManager.kt @@ -17,9 +17,10 @@ object NovaThemeManager { private const val KEY_THEME = "nova_theme" const val THEME_POLARIS = "polaris" + const val THEME_PORTABLE_CHROME = "portable_chrome" + private const val THEME_PSP = "psp" const val THEME_OLED = "oled" const val THEME_MIAMI = "miami" - const val THEME_PORTABLE_CHROME = "portable_chrome" const val THEME_HIGH_CONTRAST = "high_contrast" const val THEME_MATERIAL_YOU = "material_you" @@ -31,12 +32,12 @@ object NovaThemeManager { val isSettings = activity is com.papi.nova.preferences.StreamSettings when { + theme == THEME_PORTABLE_CHROME && isSettings -> activity.setTheme(R.style.SettingsTheme_PortableChrome) + theme == THEME_PORTABLE_CHROME -> activity.setTheme(R.style.AppTheme_PortableChrome) theme == THEME_OLED && isSettings -> activity.setTheme(R.style.SettingsTheme_OLED) theme == THEME_OLED -> activity.setTheme(R.style.AppTheme_OLED) theme == THEME_MIAMI && isSettings -> activity.setTheme(R.style.SettingsTheme_Miami) theme == THEME_MIAMI -> activity.setTheme(R.style.AppTheme_Miami) - theme == THEME_PORTABLE_CHROME && isSettings -> activity.setTheme(R.style.SettingsTheme_PortableChrome) - theme == THEME_PORTABLE_CHROME -> activity.setTheme(R.style.AppTheme_PortableChrome) theme == THEME_HIGH_CONTRAST && isSettings -> activity.setTheme(R.style.SettingsTheme_HighContrast) theme == THEME_HIGH_CONTRAST -> activity.setTheme(R.style.AppTheme_HighContrast) theme == THEME_MATERIAL_YOU && isSettings -> activity.setTheme(R.style.SettingsTheme_MaterialYou) @@ -73,7 +74,8 @@ object NovaThemeManager { private fun normalizeTheme(theme: String?): String { return when (theme) { - THEME_POLARIS, THEME_OLED, THEME_MIAMI, THEME_PORTABLE_CHROME, THEME_HIGH_CONTRAST, THEME_MATERIAL_YOU -> theme + THEME_POLARIS, THEME_PORTABLE_CHROME, THEME_OLED, THEME_MIAMI, THEME_HIGH_CONTRAST, THEME_MATERIAL_YOU -> theme + THEME_PSP -> THEME_PORTABLE_CHROME else -> THEME_POLARIS } } @@ -87,18 +89,18 @@ object NovaThemeManager { .edit().putString(KEY_THEME, normalizedTheme).apply() } + fun isPortableChrome(context: Context): Boolean = getTheme(context) == THEME_PORTABLE_CHROME fun isOled(context: Context): Boolean = getTheme(context) == THEME_OLED fun isMiami(context: Context): Boolean = getTheme(context) == THEME_MIAMI - fun isPortableChrome(context: Context): Boolean = getTheme(context) == THEME_PORTABLE_CHROME fun isHighContrast(context: Context): Boolean = getTheme(context) == THEME_HIGH_CONTRAST fun isMaterialYou(context: Context): Boolean = getTheme(context) == THEME_MATERIAL_YOU fun cycleTheme(context: Context): String { val next = when (getTheme(context)) { - THEME_POLARIS -> THEME_OLED + THEME_POLARIS -> THEME_PORTABLE_CHROME + THEME_PORTABLE_CHROME -> THEME_OLED THEME_OLED -> THEME_MIAMI - THEME_MIAMI -> THEME_PORTABLE_CHROME - THEME_PORTABLE_CHROME -> THEME_HIGH_CONTRAST + THEME_MIAMI -> THEME_HIGH_CONTRAST THEME_HIGH_CONTRAST -> if (isMaterialYouAvailable()) THEME_MATERIAL_YOU else THEME_POLARIS THEME_MATERIAL_YOU -> THEME_POLARIS else -> THEME_POLARIS @@ -109,9 +111,9 @@ object NovaThemeManager { fun getThemeLabel(context: Context, theme: String = getTheme(context)): String { return when (theme) { + THEME_PORTABLE_CHROME -> context.getString(R.string.nova_theme_portable_chrome_label) THEME_OLED -> context.getString(R.string.nova_theme_oled_label) THEME_MIAMI -> context.getString(R.string.nova_theme_miami_label) - THEME_PORTABLE_CHROME -> context.getString(R.string.nova_theme_portable_chrome_label) THEME_HIGH_CONTRAST -> context.getString(R.string.nova_theme_high_contrast_label) THEME_MATERIAL_YOU -> context.getString(R.string.nova_theme_material_you_label) else -> context.getString(R.string.nova_theme_polaris_label) @@ -134,9 +136,9 @@ object NovaThemeManager { /** Returns the correct window background color for the current theme */ fun getWindowBackgroundColor(context: Context): Int { return when { + isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_bg_window) isOled(context) -> Color.BLACK isMiami(context) -> ContextCompat.getColor(context, R.color.nova_miami_bg_window) - isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_bg_window) isHighContrast(context) -> ContextCompat.getColor(context, R.color.nova_hc_bg_window) isMaterialYou(context) && isMaterialYouAvailable() -> resolveThemeColor(context, android.R.attr.colorBackground, ContextCompat.getColor(context, R.color.nova_bg_window)) @@ -165,9 +167,9 @@ object NovaThemeManager { /** Returns the correct card background color for the current theme */ fun getCardBackgroundColor(context: Context): Int { return when { + isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_bg_card) isOled(context) -> ContextCompat.getColor(context, R.color.nova_oled_bg_card) isMiami(context) -> ContextCompat.getColor(context, R.color.nova_miami_bg_card) - isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_bg_card) isHighContrast(context) -> ContextCompat.getColor(context, R.color.nova_hc_bg_card) isMaterialYou(context) && isMaterialYouAvailable() -> resolveThemeColor(context, com.google.android.material.R.attr.colorSurface, ContextCompat.getColor(context, R.color.nova_bg_card)) @@ -178,9 +180,9 @@ object NovaThemeManager { /** Returns the correct dialog background color for the current theme */ fun getDialogBackgroundColor(context: Context): Int { return when { + isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_dialog_bg) isOled(context) -> ContextCompat.getColor(context, R.color.nova_oled_dialog_bg) isMiami(context) -> ContextCompat.getColor(context, R.color.nova_miami_dialog_bg) - isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_dialog_bg) isHighContrast(context) -> ContextCompat.getColor(context, R.color.nova_hc_dialog_bg) isMaterialYou(context) && isMaterialYouAvailable() -> resolveThemeColor(context, com.google.android.material.R.attr.colorSurface, ContextCompat.getColor(context, R.color.nova_dialog_bg)) @@ -203,31 +205,31 @@ object NovaThemeManager { } } return when { + isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_accent) isOled(context) -> ContextCompat.getColor(context, R.color.nova_oled_accent) isMiami(context) -> ContextCompat.getColor(context, R.color.nova_miami_accent) - isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_accent) isHighContrast(context) -> ContextCompat.getColor(context, R.color.nova_hc_accent) - else -> ContextCompat.getColor(context, R.color.nova_accent) + else -> ContextCompat.getColor(context, R.color.nova_polaris_accent) } } /** Returns the correct low-emphasis accent surface for the current theme */ fun getAccentSurfaceColor(context: Context): Int { return when { + isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_accent_surface) isOled(context) -> ContextCompat.getColor(context, R.color.nova_oled_accent_surface) isMiami(context) -> ContextCompat.getColor(context, R.color.nova_miami_accent_surface) - isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_accent_surface) isHighContrast(context) -> ContextCompat.getColor(context, R.color.nova_hc_accent_surface) - else -> ContextCompat.getColor(context, R.color.nova_accent_surface) + else -> ContextCompat.getColor(context, R.color.nova_polaris_accent_surface) } } /** Returns the correct divider color for the current theme */ fun getDividerColor(context: Context): Int { return when { + isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_divider) isOled(context) -> ContextCompat.getColor(context, R.color.nova_oled_divider) isMiami(context) -> ContextCompat.getColor(context, R.color.nova_miami_divider) - isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_divider) isHighContrast(context) -> ContextCompat.getColor(context, R.color.nova_hc_divider) isMaterialYou(context) && isMaterialYouAvailable() -> resolveThemeColor(context, com.google.android.material.R.attr.colorOutline, ContextCompat.getColor(context, R.color.nova_divider)) @@ -238,9 +240,9 @@ object NovaThemeManager { /** Returns the correct text primary color for the current theme */ fun getTextPrimaryColor(context: Context): Int { return when { + isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_text_primary) isOled(context) -> ContextCompat.getColor(context, R.color.nova_oled_text_primary) isMiami(context) -> ContextCompat.getColor(context, R.color.nova_miami_text_primary) - isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_text_primary) isHighContrast(context) -> ContextCompat.getColor(context, R.color.nova_hc_text_primary) isMaterialYou(context) && isMaterialYouAvailable() -> resolveThemeColor(context, android.R.attr.textColorPrimary, ContextCompat.getColor(context, R.color.nova_text_primary)) @@ -251,9 +253,9 @@ object NovaThemeManager { /** Returns the correct text secondary color for the current theme */ fun getTextSecondaryColor(context: Context): Int { return when { + isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_text_secondary) isOled(context) -> ContextCompat.getColor(context, R.color.nova_oled_text_secondary) isMiami(context) -> ContextCompat.getColor(context, R.color.nova_miami_text_secondary) - isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_text_secondary) isHighContrast(context) -> ContextCompat.getColor(context, R.color.nova_hc_text_secondary) isMaterialYou(context) && isMaterialYouAvailable() -> resolveThemeColor(context, android.R.attr.textColorSecondary, ContextCompat.getColor(context, R.color.nova_text_secondary)) @@ -264,9 +266,9 @@ object NovaThemeManager { /** Returns the correct text muted color for the current theme */ fun getTextMutedColor(context: Context): Int { return when { + isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_text_muted) isOled(context) -> ContextCompat.getColor(context, R.color.nova_oled_text_muted) isMiami(context) -> ContextCompat.getColor(context, R.color.nova_miami_text_muted) - isPortableChrome(context) -> ContextCompat.getColor(context, R.color.nova_portable_text_muted) isHighContrast(context) -> ContextCompat.getColor(context, R.color.nova_hc_text_muted) isMaterialYou(context) && isMaterialYouAvailable() -> resolveThemeColor(context, android.R.attr.textColorSecondary, ContextCompat.getColor(context, R.color.nova_text_muted)) diff --git a/app/src/main/java/com/papi/nova/ui/compose/NovaComposeTheme.kt b/app/src/main/java/com/papi/nova/ui/compose/NovaComposeTheme.kt index e1c46110..84e02ac6 100644 --- a/app/src/main/java/com/papi/nova/ui/compose/NovaComposeTheme.kt +++ b/app/src/main/java/com/papi/nova/ui/compose/NovaComposeTheme.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import com.papi.nova.R import com.papi.nova.ui.NovaThemeManager +import com.papi.nova.ui.NovaSheetChrome @Immutable data class NovaComposeColors( @@ -56,8 +57,8 @@ private fun defaultNovaComposeColors(): NovaComposeColors { dialog = Color(0xFF232340), badge = Color(0x33687B81), divider = Color(0xFF393C51), - accent = Color(0xFF7C73FF), - accentSurface = Color(0x1A7C73FF), + accent = Color(0xFF78A6FF), + accentSurface = Color(0x1A78A6FF), warning = Color(0xFFFBBF24), textPrimary = Color(0xFFD4DDE8), textSecondary = Color(0xFFA8B0B8), @@ -77,61 +78,53 @@ val LocalNovaLibrarySurfaces = staticCompositionLocalOf { fun NovaComposeColors.librarySurfaces(theme: String): NovaLibrarySurfaces { val isOled = theme == NovaThemeManager.THEME_OLED val isMiami = theme == NovaThemeManager.THEME_MIAMI - val isPortableChrome = theme == NovaThemeManager.THEME_PORTABLE_CHROME val isHighContrast = theme == NovaThemeManager.THEME_HIGH_CONTRAST val isMaterialYou = theme == NovaThemeManager.THEME_MATERIAL_YOU return NovaLibrarySurfaces( backgroundScrim = when { isOled -> Color.Transparent isMiami -> window.copy(alpha = 0.60f) - isPortableChrome -> Color.Black.copy(alpha = 0.28f) isHighContrast -> Color.Black.copy(alpha = 0.72f) isMaterialYou -> window.copy(alpha = 0.28f) else -> window.copy(alpha = 0.56f) }, panel = when { - isOled -> dialog.copy(alpha = 0.88f) - isMiami -> dialog.copy(alpha = 0.82f) - isPortableChrome -> dialog.copy(alpha = 0.94f) - isHighContrast -> dialog.copy(alpha = 0.96f) - isMaterialYou -> card.copy(alpha = 0.76f) - else -> dialog.copy(alpha = 0.64f) + isOled -> dialog.copy(alpha = NovaSheetChrome.OLED_SHEET_GLASS_ALPHA) + isMiami -> dialog.copy(alpha = NovaSheetChrome.MIAMI_SHEET_GLASS_ALPHA) + isHighContrast -> dialog.copy(alpha = NovaSheetChrome.HIGH_CONTRAST_SHEET_GLASS_ALPHA) + isMaterialYou -> card.copy(alpha = NovaSheetChrome.MATERIAL_YOU_SHEET_GLASS_ALPHA) + else -> dialog.copy(alpha = NovaSheetChrome.SHEET_GLASS_ALPHA) }, panelBorder = when { isOled -> divider.copy(alpha = 0.78f) isMiami -> accent.copy(alpha = 0.18f) - isPortableChrome -> divider.copy(alpha = 0.70f) isHighContrast -> divider.copy(alpha = 0.92f) isMaterialYou -> divider.copy(alpha = 0.46f) else -> divider.copy(alpha = 0.44f) }, tile = when { - isOled -> card.copy(alpha = 0.90f) - isMiami -> card.copy(alpha = 0.82f) - isPortableChrome -> card.copy(alpha = 0.92f) + isOled -> card.copy(alpha = 0.82f) + isMiami -> card.copy(alpha = 0.70f) isHighContrast -> card.copy(alpha = 0.98f) - isMaterialYou -> card.copy(alpha = 0.78f) - else -> card.copy(alpha = 0.74f) + isMaterialYou -> card.copy(alpha = 0.68f) + else -> card.copy(alpha = 0.66f) }, tileBorder = when { isOled -> divider.copy(alpha = 0.78f) isMiami -> divider.copy(alpha = 0.58f) - isPortableChrome -> divider.copy(alpha = 0.62f) isHighContrast -> divider.copy(alpha = 0.90f) else -> divider.copy(alpha = 0.50f) }, control = when { - isOled -> card.copy(alpha = 0.78f) - isMiami -> card.copy(alpha = 0.76f) - isPortableChrome -> card.copy(alpha = 0.88f) + isOled -> card.copy(alpha = 0.74f) + isMiami -> card.copy(alpha = 0.64f) isHighContrast -> card.copy(alpha = 1f) - isMaterialYou -> card.copy(alpha = 0.70f) - else -> card.copy(alpha = 0.72f) + isMaterialYou -> card.copy(alpha = 0.62f) + else -> card.copy(alpha = 0.60f) }, selectedControl = accent.copy(alpha = when { isHighContrast -> 0.34f isMiami -> 0.22f - isPortableChrome -> 0.18f isOled -> 0.22f else -> 0.18f }), @@ -139,14 +132,12 @@ fun NovaComposeColors.librarySurfaces(theme: String): NovaLibrarySurfaces { focusHalo = accent.copy(alpha = when { isHighContrast -> 0.36f isMiami -> 0.28f - isPortableChrome -> 0.16f isOled -> 0.24f else -> 0.18f }), mediaPlaceholder = when { isOled -> Color(0xFF08080C) isMiami -> Color(0xFF2C1734) - isPortableChrome -> Color(0xFFA2ADBA) isHighContrast -> Color(0xFF111827) isMaterialYou -> card.copy(alpha = 1f) else -> divider.copy(alpha = 1f) @@ -162,14 +153,12 @@ fun NovaComposeColors.librarySurfaces(theme: String): NovaLibrarySurfaces { focusedArtworkAlpha = when { isOled -> 0.10f isMiami -> 0.26f - isPortableChrome -> 0.12f isMaterialYou -> 0.18f else -> 0.24f }, focusedArtworkScrim = Color.Black.copy(alpha = when { isOled -> 0.82f isMiami -> 0.76f - isPortableChrome -> 0.70f else -> 0.72f }), particlesEnabled = !isOled, @@ -177,7 +166,6 @@ fun NovaComposeColors.librarySurfaces(theme: String): NovaLibrarySurfaces { isOled -> 0f isHighContrast -> 0.28f isMiami -> 0.68f - isPortableChrome -> 0.24f isMaterialYou -> 0.42f else -> 1f } @@ -203,11 +191,7 @@ fun NovaComposeTheme(content: @Composable () -> Unit) { onAccent = Color( ContextCompat.getColor( context, - when (theme) { - NovaThemeManager.THEME_MIAMI -> R.color.nova_miami_void - NovaThemeManager.THEME_PORTABLE_CHROME -> R.color.nova_portable_on_accent - else -> R.color.nova_ice - } + if (theme == NovaThemeManager.THEME_MIAMI) R.color.nova_miami_void else R.color.nova_ice ) ) ) diff --git a/app/src/main/res/color/nova_chip_bg_selector.xml b/app/src/main/res/color/nova_chip_bg_selector.xml index 9c117f14..982aeb5d 100644 --- a/app/src/main/res/color/nova_chip_bg_selector.xml +++ b/app/src/main/res/color/nova_chip_bg_selector.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/color/nova_focus_stroke_selector.xml b/app/src/main/res/color/nova_focus_stroke_selector.xml index e8bf525f..6ee856d6 100644 --- a/app/src/main/res/color/nova_focus_stroke_selector.xml +++ b/app/src/main/res/color/nova_focus_stroke_selector.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/res/drawable/nova_chip_selected.xml b/app/src/main/res/drawable/nova_chip_selected.xml index db73bd13..d8b33ac1 100644 --- a/app/src/main/res/drawable/nova_chip_selected.xml +++ b/app/src/main/res/drawable/nova_chip_selected.xml @@ -5,7 +5,7 @@ + android:color="?attr/colorOnSurface" /> diff --git a/app/src/main/res/drawable/nova_featured_action_bg.xml b/app/src/main/res/drawable/nova_featured_action_bg.xml index b7248d58..fcd164f6 100644 --- a/app/src/main/res/drawable/nova_featured_action_bg.xml +++ b/app/src/main/res/drawable/nova_featured_action_bg.xml @@ -5,5 +5,5 @@ + android:color="?attr/colorAccent" /> diff --git a/app/src/main/res/drawable/nova_server_row_focus_ring.xml b/app/src/main/res/drawable/nova_server_row_focus_ring.xml index de7480ab..7da3c0a6 100644 --- a/app/src/main/res/drawable/nova_server_row_focus_ring.xml +++ b/app/src/main/res/drawable/nova_server_row_focus_ring.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/main/res/layout-land/activity_pc_view.xml b/app/src/main/res/layout-land/activity_pc_view.xml index 0f6c0828..b0d14db2 100644 --- a/app/src/main/res/layout-land/activity_pc_view.xml +++ b/app/src/main/res/layout-land/activity_pc_view.xml @@ -461,7 +461,8 @@ android:layout_width="32dp" android:layout_height="32dp" android:layout_marginTop="20dp" - android:indeterminate="true" /> + android:indeterminate="true" + android:indeterminateTint="@color/nova_accent" /> + android:indeterminate="true" + android:indeterminateTint="@color/nova_accent" /> @@ -22,7 +22,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="18sp" - android:textColor="@color/nova_ice" + android:textColor="?android:textColorPrimary" android:fontFamily="sans-serif-medium" android:paddingStart="24dp" android:paddingEnd="24dp" diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index a07ec0e7..284416b8 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -2,17 +2,17 @@ Polaris Aurora + PSP Chrome / Portable Chrome Console OLED Miami Nebula - PSP / Portable Chrome High Contrast Material You polaris + portable_chrome oled miami - portable_chrome high_contrast material_you diff --git a/app/src/main/res/values/colors_nova.xml b/app/src/main/res/values/colors_nova.xml index 2c037d97..1e0b2476 100644 --- a/app/src/main/res/values/colors_nova.xml +++ b/app/src/main/res/values/colors_nova.xml @@ -55,32 +55,6 @@ #73FF5CAB #22FF5CAB - - #FF7C8998 - #FFA2ADBA - #FF8E9BAA - #FF667484 - #FFC4CDD8 - #FFD7DEE7 - #FFA2ADBA - #FFA2ADBA - #E6C4CDD8 - #FFCBD3DD - #FFB1BCC9 - #FFC0CAD5 - #FFAFBAC7 - #FF667484 - #FF1F2A35 - #FF465464 - #FF5A6877 - #FF4B6686 - #264B6686 - #334B6686 - #244B6686 - #FF294F3D - #FF294F3D - #FF4B6686 - #FFFFFFFF #FF05070c #F00f172a @@ -100,15 +74,34 @@ #FFa8b0b8 #FF7a8e95 - #FF7c73ff - #336c63ff - #1A7c73ff + #FF78A6FF + #3378A6FF + #1A78A6FF + #2278A6FF + + #FFA2ADBA + #FFC4CDD8 + #FFCBD3DD + #FFB1BCC9 + #FFC4CDD8 + #FF83909F + #FF1F2A35 + #FF3B4856 + #FF5E6B79 + #FF7FA38D + #337FA38D + #1A7FA38D + #227FA38D + + @color/nova_polaris_accent + @color/nova_polaris_accent_glow + @color/nova_polaris_accent_surface #FF4ade80 #FFfbbf24 #FFf87171 #FF393c51 - #226c63ff + @color/nova_polaris_ripple #FF252545 @@ -121,5 +114,5 @@ #FF4ade80 #FFfbbf24 #FFf87171 - #FF7c73ff + #FF7FA38D diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebd2cce9..4f5b1498 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,9 +48,9 @@ Cycle theme Theme: %1$s Polaris Aurora + PSP Chrome / Portable Chrome Console OLED Miami Nebula - PSP / Portable Chrome High Contrast Material You Open Nova Library @@ -78,6 +78,15 @@ GitHub Settings Choose theme + D-pad moves focus. Select a theme to apply. + Focused: %1$s · %2$s + Current + Polaris blue cockpit · balanced dark streaming shell + PSP / Portable Chrome profile · smoked graphite, dim silver, muted green accents + OLED black · high contrast console glow + Miami neon · warm rose and cyan night drive + Maximum contrast · accessibility-first focus states + System dynamic colors when Android supports them All Online Streaming @@ -1167,4 +1176,5 @@ Enable Full Screen Hide system bars with immersive mode + Nova game stream surface diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1e15d793..825b3213 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,10 +6,10 @@ by AppBaseTheme from res/values-vXX/styles.xml on newer devices. --> + + + @@ -58,21 +78,6 @@ @color/nova_miami_text_primary - - + + -