Skip to content

RFC: Preliminary work for configurable navigation bar and top buttons#3279

Closed
Lurux wants to merge 10 commits intoMetrolistGroup:mainfrom
Lurux:BaseAppbarConfig
Closed

RFC: Preliminary work for configurable navigation bar and top buttons#3279
Lurux wants to merge 10 commits intoMetrolistGroup:mainfrom
Lurux:BaseAppbarConfig

Conversation

@Lurux
Copy link
Copy Markdown

@Lurux Lurux commented Mar 18, 2026

This PR implements a new NavigationScreens enum, used to store and configure the position of buttons for the main navigation screens in the interface (top bar, navigation bar, automatic, hidden), replacing the old Screens enum.

This PR does not implement any user-facing changes on its own, instead it will be used as the basis for upcoming PRs.

Implementation

The positioning logic is implemented as follows:

A NavigationItemPosition enum is used to store the position a given button should take in the interface:

  • NAV_BAR: Forcibly show in the navigation bar. Don't show on the top bar.
  • TOP_BAR: Forcibly show on the top bar. Don't show in the navigation bar.
  • AUTOMATIC: Items set to automatic will be placed in the navigation bar until 5 items are housed in there (as per M3E's recommendation, configurable through MAX_ITEMS_IN_NAV_BAR), the remaining automatically positioned items will be shown in the top bar instead.
  • HIDDEN: Hidden

A NavigationItemType enum is used to differentiate between different item types:

  • CORE: These are core pages of the application and their position shall not be made configurable by the user
  • LIBRARY: These are library pages and thus should not appear in the top bar (not applicable)
  • OTHER: Other pages (such as search and history) with no restriction for their positioning

The home and library pages were implemented as NavigationScreens to simplify the codebase, hence they've been marked as CORE to avoid misconfigurations. CORE items have been hard-coded to always be included in NavigationScreens's getNavbarItems function

The NavigationScreens enum serves as the main class holding everything together, including the following information about each entry:

  • titleId (from Screens): Title ressource id
  • iconIdInactive (from Screens): Inactive icon ressource id
  • iconIdActive (from Screens): Active icon ressource id
  • route (from Screens): Route to desired screen
  • key: Preferences key holding the current item position
  • type: NavigationItemType
  • default_position: Default NavigationItemPosition to be stored at the configured preferences key

The NavigationScreens enum additionally has the following helper functions:

  • NavigationScreens.XXX.position(): Get the currently configured position
  • NavigationScreens.getNavbarItems(): Get the list of items to be displayed in the navigation bar. This includes items positioned to NAV_BAR, but also items positioned to AUTO until MAX_ITEMS_IN_NAV_BAR is reached.
  • NavigationScreens.getTopbarItems(includeHidden: Boolean = false): Essentially the opposite of getNavbarItems, returning the items that should be shown in the top bar (optionally including HIDDEN items, for future usage)

Current use

The app has been adapted to use this new mechanism to build the navigation bar and populate the top bar icons, with no changes to the settings or default behavior compared to the current state of the app. The search tab has been made visible by default and the positioning of the "Listen together" button bound to the "Listen together in the top bar" setting item.

Specifics

The following buttons are now implemented as NavigationScreens:

  • Home (position: NAV_BAR, type: CORE)
  • Search (position: AUTOMATIC)
  • History (position: TOP_BAR)
  • Stats (position: TOP_BAR)
  • Listen together (position: TOP_BAR or NAV_BAR via the settings)
  • Library (position: NAV_BAR)

Future use

This PR is to be used in upcoming works in order to allow adding/removing items from the top bar, and/or pinning library pages or other items to the navigation bar, through a flexible and coherent underlying system.

Any reviews and comments welcome !

Cheers,
Lurux

Summary by CodeRabbit

Release Notes

  • Refactor

    • Reworked navigation model to a centralized, position-driven system controlling navbar/topbar/hidden placement and visibility.
    • Top-bar and navigation rendering now derive from that model for consistent titles, icons, and visibility.
  • New Features

    • Configurable navigation positions with a capped navbar capacity and automatic fallback placements.
    • Settings updated to manage default open tab and navigation item positions; Listen Together visibility adapts to placement.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

Refactors navigation from a sealed-class model (Screens) to an enum-driven NavigationScreens with position-based placement, updates MainActivity and navigation components to use the new enum, moves Listen Together visibility into navigation configuration, and removes the old Screens and related preference key.

Changes

Cohort / File(s) Summary
Navigation model & constants
app/src/main/kotlin/com/metrolist/music/constants/NavigationScreens.kt, app/src/main/kotlin/com/metrolist/music/constants/PreferenceKeys.kt
Added NavigationScreens, NavigationItemPosition, NavigationItemType, and MAX_ITEMS_IN_NAV_BAR; removed ListenTogetherInTopBarKey. Provides composable helpers getNavbarItems() and getTopbarItems() and persistent position keys.
Main activity & top-level navigation
app/src/main/kotlin/com/metrolist/music/MainActivity.kt, app/src/main/kotlin/com/metrolist/music/ui/screens/NavigationBuilder.kt
Replaced Screens/NavigationTab usage with NavigationScreens; derive navigationItems from NavigationScreens.getNavbarItems(); simplified startDestination and top-bar/title resolution to use NavigationScreens routes.
Navigation UI components
app/src/main/kotlin/com/metrolist/music/ui/component/AppNavigation.kt
Updated AppNavigationBar and AppNavigationRail APIs and internal logic to accept/operate on NavigationScreens; callback signatures changed to (NavigationScreens, Boolean).
Listen Together & settings updates
app/src/main/kotlin/com/metrolist/music/ui/screens/ListenTogetherScreen.kt, app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt
Listen Together visibility now derived from NavigationScreens membership; settings replaced boolean preference with enum-position controls and updated default-open-tab flow to set navigation positions.
Removed legacy model
app/src/main/kotlin/com/metrolist/music/ui/screens/Screens.kt
Deleted the Screens sealed class and its screen objects/companion list; callers migrated to NavigationScreens.
Navigation utilities
app/src/main/kotlin/com/metrolist/music/ui/utils/NavControllerUtils.kt
Simplified NavController.backToMain() to pop until there is no previous back stack entry (removed route-based stop logic).

Sequence Diagram(s)

sequenceDiagram
    participant UI as rgba(52,152,219,0.5) UI (TopBar/BottomBar)
    participant Nav as rgba(46,204,113,0.5) NavController
    participant Pref as rgba(155,89,182,0.5) DataStore/Preferences
    participant Router as rgba(241,196,15,0.5) NavigationBuilder

    UI->>Pref: read NavigationScreens.getNavbarItems()
    Pref-->>UI: list of navbar items
    UI->>UI: render nav bar & top bar using items
    UI->>Nav: onItemClick(route)
    Nav->>Router: navigate to route
    Router-->>Nav: route composable invoked
    Nav->>Pref: (optional) update NavigationScreens position via setter
    Pref-->>Nav: ack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • nyxiereal
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main objective: refactoring navigation to use a configurable NavigationScreens enum replacing the legacy Screens enum, enabling future UI customization.
Description check ✅ Passed The description comprehensively covers the problem (legacy enum limitation), solution (NavigationScreens with position/type enums), implementation details, current behavior changes, and future roadmap.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch BaseAppbarConfig

Comment @coderabbitai help to get the list of available commands and usage tips.

@Lurux
Copy link
Copy Markdown
Author

Lurux commented Mar 24, 2026

Update: changed everything to use NavigationScreens and removed legacy Screens enum. "Default open tab" now allows setting any of the NavigationScreens by default, I made it so selecting an item will pin it to the navigation bar by default, then revert to default config once unpinned.

PR is ready for review.

@Lurux Lurux marked this pull request as ready for review March 24, 2026 18:31
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/kotlin/com/metrolist/music/ui/component/AppNavigation.kt (1)

42-46: ⚠️ Potential issue | 🟡 Minor

Keep Search selected on result screens.

NavigationScreens.SEARCH.route is search_input, so a route like search/{query} never satisfies startsWith("$screenRoute/"). The rail/bar drops Search’s selected state as soon as the user opens results. MainActivity already special-cases "search/", so this helper should mirror that rule.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/ui/component/AppNavigation.kt` around
lines 42 - 46, The selection logic in isRouteSelected fails to keep Search
selected because NavigationScreens.SEARCH.route == "search_input" and routes
like "search/{query}" won't match startsWith("$screenRoute/"); update
isRouteSelected to special-case NavigationScreens.SEARCH.route (or when
screenRoute equals that value) and also return true when
currentRoute.startsWith("search/") (mirroring MainActivity's "search/" rule),
while keeping the existing exact-equals and startsWith("$screenRoute/") checks
for other screens.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/kotlin/com/metrolist/music/constants/NavigationScreens.kt`:
- Around line 50-57: The SEARCH enum entry in NavigationScreens is using the
same persisted key as Home (stringPreferencesKey("nav_home_position")), which
couples their saved positions; change the SEARCH entry to use its own
Preferences key (e.g., stringPreferencesKey("nav_search_position")) so Search
position is persisted separately. Locate the SEARCH enum constant in
NavigationScreens and replace the stringPreferencesKey("nav_home_position")
argument with stringPreferencesKey("nav_search_position"); ensure any code that
reads/writes that key (if present) is updated to the new key name to maintain
consistency.

In `@app/src/main/kotlin/com/metrolist/music/MainActivity.kt`:
- Around line 575-592: The logic building topLevelScreens and resolving
currentTitleRes only uses NavigationScreens.getNavbarItems(), causing TopAppBar
visibility and title lookup to miss routes from
NavigationScreens.getTopbarItems(); update the code that constructs
topLevelScreens (and any title-resolution logic that references it, e.g.,
currentTitleRes) to include routes from getTopbarItems() as well (merge
getNavbarItems() and getTopbarItems() results before mapping to .route) so
History/Stats/AUTOMATIC overflow routes are treated as top-level and the title
lookup and TopAppBar visibility remain correct.
- Around line 583-588: The intent-action-to-screen mapping in the remember block
is inverted: when intent?.action == ACTION_SEARCH it should navigate to
NavigationScreens.SEARCH and when intent?.action == ACTION_LIBRARY it should
navigate to NavigationScreens.LIBRARY; update the when expression inside the
remember block (the mapping that currently maps ACTION_SEARCH ->
NavigationScreens.LIBRARY and ACTION_LIBRARY -> NavigationScreens.SEARCH) to
swap those targets so each ACTION_* constant points to the matching
NavigationScreens.* value.
- Around line 857-864: The refactor unconditionally renders the History icon by
iterating NavigationScreens.getTopbarItems() despite the existing
showHistoryButton gate; update the rendering to respect showHistoryButton by
filtering or conditionalizing so NavigationScreens.HISTORY is only included when
showHistoryButton is true—e.g., in the block that currently loops over
NavigationScreens.getTopbarItems(), skip or exclude the item whose route equals
NavigationScreens.HISTORY (or check item == NavigationScreens.HISTORY) unless
showHistoryButton is true before creating the IconButton/navController.navigate
call.

In
`@app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt`:
- Around line 1456-1459: The Switch's onCheckedChange is currently returning a
NavigationItemPosition instead of invoking the state callback and the mapping is
inverted; update the onCheckedChange lambda to call
onListenTogetherPositionChange(...) and pass NavigationItemPosition.TOP_BAR when
checked is true and NavigationItemPosition.NAV_BAR when checked is false (use
the existing listenTogetherInTopBar, onListenTogetherPositionChange, Switch, and
NavigationItemPosition identifiers to locate the code).

In `@app/src/main/kotlin/com/metrolist/music/ui/utils/NavControllerUtils.kt`:
- Around line 10-12: The backToMain() loop currently keeps popping until there
is no previousBackStackEntry, which also removes the selected top-level tab;
change the loop to stop when the next entry is a top-level destination by
checking the destination id against the nav graph start/top-level ids. Update
NavController.backToMain() to pop while previousBackStackEntry != null AND
previousBackStackEntry.destination.id != graph.startDestinationId (or not in
your set of top-level destination ids), using previousBackStackEntry,
popBackStack(), and graph.startDestinationId (or your topLevelIds set) to detect
and preserve the current top-level tab.

---

Outside diff comments:
In `@app/src/main/kotlin/com/metrolist/music/ui/component/AppNavigation.kt`:
- Around line 42-46: The selection logic in isRouteSelected fails to keep Search
selected because NavigationScreens.SEARCH.route == "search_input" and routes
like "search/{query}" won't match startsWith("$screenRoute/"); update
isRouteSelected to special-case NavigationScreens.SEARCH.route (or when
screenRoute equals that value) and also return true when
currentRoute.startsWith("search/") (mirroring MainActivity's "search/" rule),
while keeping the existing exact-equals and startsWith("$screenRoute/") checks
for other screens.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fcbe0d0f-94ba-4739-a765-f5e263c71b6d

📥 Commits

Reviewing files that changed from the base of the PR and between afa1b58 and 3cde0f3.

📒 Files selected for processing (9)
  • app/src/main/kotlin/com/metrolist/music/MainActivity.kt
  • app/src/main/kotlin/com/metrolist/music/constants/NavigationScreens.kt
  • app/src/main/kotlin/com/metrolist/music/constants/PreferenceKeys.kt
  • app/src/main/kotlin/com/metrolist/music/ui/component/AppNavigation.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/ListenTogetherScreen.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/NavigationBuilder.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/Screens.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt
  • app/src/main/kotlin/com/metrolist/music/ui/utils/NavControllerUtils.kt
💤 Files with no reviewable changes (2)
  • app/src/main/kotlin/com/metrolist/music/constants/PreferenceKeys.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/Screens.kt

Comment thread app/src/main/kotlin/com/metrolist/music/MainActivity.kt
Comment thread app/src/main/kotlin/com/metrolist/music/MainActivity.kt
Comment on lines +857 to 864
NavigationScreens.getTopbarItems().forEach { item ->
IconButton(onClick = { navController.navigate(item.route) }) {
Icon(
painter = painterResource(R.drawable.group_outlined),
contentDescription = stringResource(R.string.together),
painter = painterResource(item.iconIdInactive),
contentDescription = stringResource(item.titleId),
)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Preserve the History visibility gate.

This generic loop renders NavigationScreens.HISTORY unconditionally. The old showHistoryButton rule is still computed above, so the refactor has brought the History icon back even when listen history is paused and empty.

Suggested fix
-                                            NavigationScreens.getTopbarItems().forEach { item ->
+                                            NavigationScreens.getTopbarItems()
+                                                .filterNot { it == NavigationScreens.HISTORY && !showHistoryButton }
+                                                .forEach { item ->
                                                 IconButton(onClick = { navController.navigate(item.route) }) {
                                                     Icon(
                                                         painter = painterResource(item.iconIdInactive),
                                                         contentDescription = stringResource(item.titleId),
                                                     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
NavigationScreens.getTopbarItems().forEach { item ->
IconButton(onClick = { navController.navigate(item.route) }) {
Icon(
painter = painterResource(R.drawable.group_outlined),
contentDescription = stringResource(R.string.together),
painter = painterResource(item.iconIdInactive),
contentDescription = stringResource(item.titleId),
)
}
}
NavigationScreens.getTopbarItems()
.filterNot { it == NavigationScreens.HISTORY && !showHistoryButton }
.forEach { item ->
IconButton(onClick = { navController.navigate(item.route) }) {
Icon(
painter = painterResource(item.iconIdInactive),
contentDescription = stringResource(item.titleId),
)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/MainActivity.kt` around lines 857 -
864, The refactor unconditionally renders the History icon by iterating
NavigationScreens.getTopbarItems() despite the existing showHistoryButton gate;
update the rendering to respect showHistoryButton by filtering or
conditionalizing so NavigationScreens.HISTORY is only included when
showHistoryButton is true—e.g., in the block that currently loops over
NavigationScreens.getTopbarItems(), skip or exclude the item whose route equals
NavigationScreens.HISTORY (or check item == NavigationScreens.HISTORY) unless
showHistoryButton is true before creating the IconButton/navController.navigate
call.

Comment on lines 10 to 12
fun NavController.backToMain() {
val mainRoutes = Screens.MainScreens.map { it.route }

while (previousBackStackEntry != null &&
currentBackStackEntry?.destination?.route !in mainRoutes
) {
while (previousBackStackEntry != null) {
popBackStack()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop backToMain() at the current top-level destination.

This now pops the selected tab off the stack as well. A flow like Home -> Library -> album will long-press back to Home instead of Library, which is a regression from the previous “stop at main screen” behavior.

Suggested fix
 import androidx.navigation.NavController
+import com.metrolist.music.constants.NavigationScreens
 
 fun NavController.backToMain() {
-    while (previousBackStackEntry != null) {
+    val topLevelRoutes = NavigationScreens.entries.map { it.route }.toSet() + "listen_together_from_topbar"
+    while (
+        previousBackStackEntry != null &&
+        currentBackStackEntry?.destination?.route !in topLevelRoutes
+    ) {
         popBackStack()
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/ui/utils/NavControllerUtils.kt`
around lines 10 - 12, The backToMain() loop currently keeps popping until there
is no previousBackStackEntry, which also removes the selected top-level tab;
change the loop to stop when the next entry is a top-level destination by
checking the destination id against the nav graph start/top-level ids. Update
NavController.backToMain() to pop while previousBackStackEntry != null AND
previousBackStackEntry.destination.id != graph.startDestinationId (or not in
your set of top-level destination ids), using previousBackStackEntry,
popBackStack(), and graph.startDestinationId (or your topLevelIds set) to detect
and preserve the current top-level tab.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
app/src/main/kotlin/com/metrolist/music/MainActivity.kt (2)

591-591: ⚠️ Potential issue | 🟠 Major

Top-level screens and title lookup miss top-bar routes.

topLevelScreens is built only from getNavbarItems(), so routes like history and stats from getTopbarItems() are excluded. This affects:

  1. shouldShowTopBar logic (line 776-778) may incorrectly hide the top bar on these screens
  2. currentTitleRes lookup (line 809-812) returns null for top-bar routes, leaving the title empty
🔧 Suggested fix
-                val topLevelScreens = navigationItems.map{ it.route }
+                val topBarItems = NavigationScreens.getTopbarItems()
+                val topLevelScreens = remember(navigationItems, topBarItems) {
+                    (navigationItems + topBarItems).map { it.route }.toSet()
+                }

                 // ... later ...

                 val currentTitleRes =
                     remember(navBackStackEntry) {
-                        navigationItems.firstOrNull{ it.route == navBackStackEntry?.destination?.route }?.titleId
+                        (navigationItems + topBarItems)
+                            .distinctBy { it.route }
+                            .firstOrNull { it.route == navBackStackEntry?.destination?.route }
+                            ?.titleId
                     }

Also applies to: 809-812

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/MainActivity.kt` at line 591,
topLevelScreens is built from navigationItems (getNavbarItems()) only, so routes
returned by getTopbarItems() (e.g., history, stats) are missing which breaks
shouldShowTopBar and currentTitleRes lookups; update the construction of
topLevelScreens to include both navigationItems and topbar items by merging
routes from getNavbarItems() and getTopbarItems(), then use that combined set in
shouldShowTopBar and when resolving currentTitleRes so top-bar routes are
recognized and their titles shown.

857-864: ⚠️ Potential issue | 🟡 Minor

History button visibility gate is missing.

The refactored loop renders all items from getTopbarItems() unconditionally, including HISTORY. The existing showHistoryButton variable (computed at lines 818-821) should filter out the History icon when listen history is paused and empty.

🔧 Suggested fix
-                                            NavigationScreens.getTopbarItems().forEach { item ->
+                                            NavigationScreens.getTopbarItems()
+                                                .filter { it != NavigationScreens.HISTORY || showHistoryButton }
+                                                .forEach { item ->
                                                 IconButton(onClick = { navController.navigate(item.route) }) {
                                                     Icon(
                                                         painter = painterResource(item.iconIdInactive),
                                                         contentDescription = stringResource(item.titleId),
                                                     )
                                                 }
                                             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/MainActivity.kt` around lines 857 -
864, The loop rendering topbar icons indiscriminately uses
NavigationScreens.getTopbarItems(); update it to respect the existing
showHistoryButton flag by filtering out the HISTORY item when showHistoryButton
is false. Locate the forEach that calls
IconButton(navController.navigate(item.route)) and change the source collection
to NavigationScreens.getTopbarItems().filter { it != NavigationScreens.HISTORY
|| showHistoryButton } (or equivalent check against item id/enum), so the
History Icon is only rendered when showHistoryButton is true.
🧹 Nitpick comments (3)
app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt (1)

561-568: Consider null-safety for the position setter lookup.

The onNavigationItemPositionChange[defaultOpenTab]?.invoke(...) pattern is good, but if a new NavigationScreens entry is added without updating the map, this would silently fail.

💡 Optional: Add logging or assertion for missing keys
             onSelect = {
                 // Reset the previously selected item to its default position
-                onNavigationItemPositionChange[defaultOpenTab]?.invoke(defaultOpenTab.default_position)
+                onNavigationItemPositionChange[defaultOpenTab]?.invoke(defaultOpenTab.default_position)
+                    ?: run { /* Consider logging if key is missing */ }
                 // Update defaultOpenTab
                 onDefaultOpenTabChange(it)
                 // Pin newly selected item to navigation bar
-                onNavigationItemPositionChange[it]?.invoke(NavigationItemPosition.NAV_BAR)
+                onNavigationItemPositionChange[it]?.invoke(NavigationItemPosition.NAV_BAR)
+                    ?: run { /* Consider logging if key is missing */ }
                 // Close menu
                 showDefaultOpenTabDialog = false
             },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt`
around lines 561 - 568, The current use of
onNavigationItemPositionChange[defaultOpenTab]?.invoke(defaultOpenTab.default_position)
can silently no-op if defaultOpenTab is not present in the map; update the logic
in the block around onDefaultOpenTabChange to explicitly check for a mapping
before invoking (e.g., if
(onNavigationItemPositionChange.containsKey(defaultOpenTab)) { invoke(...) }
else { log or throw/assert }), do the same for
onNavigationItemPositionChange[it] when pinning the new item, and include a
clear log message referencing defaultOpenTab/it and NavigationItemPosition so
missing map entries are detected during development.
app/src/main/kotlin/com/metrolist/music/constants/NavigationScreens.kt (2)

37-39: Use camelCase for the default_position property.

Kotlin convention uses camelCase for property names. default_position should be defaultPosition.

♻️ Suggested refactor
 enum class NavigationScreens(
     `@StringRes` val titleId: Int,
     `@DrawableRes` val iconIdInactive: Int,
     `@DrawableRes` val iconIdActive: Int,
     val route: String,
     val key: Preferences.Key<String>,
     val type: NavigationItemType,
-    val default_position: NavigationItemPosition
+    val defaultPosition: NavigationItemPosition
 ) {

Then update all usages (e.g., it.default_positionit.defaultPosition).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/constants/NavigationScreens.kt`
around lines 37 - 39, Rename the property default_position to camelCase
defaultPosition in the NavigationScreens data holder declaration (the property
with type NavigationItemPosition alongside key: Preferences.Key<String> and
type: NavigationItemType), and update every usage site to reference
defaultPosition (e.g., replace it.default_position with it.defaultPosition) so
all callers and references compile with Kotlin naming conventions.

96-104: Composable state reads inside enum methods may cause recomposition overhead.

getPosition() and getPositionSetter() subscribe to DataStore preferences. When called in loops (e.g., getNavbarItems()), each call creates a separate state subscription. With only 6 items this is acceptable, but be mindful if the enum grows significantly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/com/metrolist/music/constants/NavigationScreens.kt`
around lines 96 - 104, getPosition() and getPositionSetter() each call
rememberEnumPreference separately which creates multiple DataStore subscriptions
when used in loops (e.g., getNavbarItems()); instead, hoist the preference read
by replacing these two methods with a single `@Composable` helper (eg.
rememberNavigationPositionPair/rememberPositionPair) that calls
rememberEnumPreference(this.key, this.default_position) once and returns both
the value and setter (value and component2) as a pair; update callers (like
getNavbarItems) to call that helper once and pass the returned value and setter
into item construction so you avoid repeated state subscriptions and extra
recompositions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/src/main/kotlin/com/metrolist/music/MainActivity.kt`:
- Line 591: topLevelScreens is built from navigationItems (getNavbarItems())
only, so routes returned by getTopbarItems() (e.g., history, stats) are missing
which breaks shouldShowTopBar and currentTitleRes lookups; update the
construction of topLevelScreens to include both navigationItems and topbar items
by merging routes from getNavbarItems() and getTopbarItems(), then use that
combined set in shouldShowTopBar and when resolving currentTitleRes so top-bar
routes are recognized and their titles shown.
- Around line 857-864: The loop rendering topbar icons indiscriminately uses
NavigationScreens.getTopbarItems(); update it to respect the existing
showHistoryButton flag by filtering out the HISTORY item when showHistoryButton
is false. Locate the forEach that calls
IconButton(navController.navigate(item.route)) and change the source collection
to NavigationScreens.getTopbarItems().filter { it != NavigationScreens.HISTORY
|| showHistoryButton } (or equivalent check against item id/enum), so the
History Icon is only rendered when showHistoryButton is true.

---

Nitpick comments:
In `@app/src/main/kotlin/com/metrolist/music/constants/NavigationScreens.kt`:
- Around line 37-39: Rename the property default_position to camelCase
defaultPosition in the NavigationScreens data holder declaration (the property
with type NavigationItemPosition alongside key: Preferences.Key<String> and
type: NavigationItemType), and update every usage site to reference
defaultPosition (e.g., replace it.default_position with it.defaultPosition) so
all callers and references compile with Kotlin naming conventions.
- Around line 96-104: getPosition() and getPositionSetter() each call
rememberEnumPreference separately which creates multiple DataStore subscriptions
when used in loops (e.g., getNavbarItems()); instead, hoist the preference read
by replacing these two methods with a single `@Composable` helper (eg.
rememberNavigationPositionPair/rememberPositionPair) that calls
rememberEnumPreference(this.key, this.default_position) once and returns both
the value and setter (value and component2) as a pair; update callers (like
getNavbarItems) to call that helper once and pass the returned value and setter
into item construction so you avoid repeated state subscriptions and extra
recompositions.

In
`@app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt`:
- Around line 561-568: The current use of
onNavigationItemPositionChange[defaultOpenTab]?.invoke(defaultOpenTab.default_position)
can silently no-op if defaultOpenTab is not present in the map; update the logic
in the block around onDefaultOpenTabChange to explicitly check for a mapping
before invoking (e.g., if
(onNavigationItemPositionChange.containsKey(defaultOpenTab)) { invoke(...) }
else { log or throw/assert }), do the same for
onNavigationItemPositionChange[it] when pinning the new item, and include a
clear log message referencing defaultOpenTab/it and NavigationItemPosition so
missing map entries are detected during development.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba5faab1-aa73-4d48-acd1-a62be8dd520e

📥 Commits

Reviewing files that changed from the base of the PR and between 3cde0f3 and e065ada.

📒 Files selected for processing (3)
  • app/src/main/kotlin/com/metrolist/music/MainActivity.kt
  • app/src/main/kotlin/com/metrolist/music/constants/NavigationScreens.kt
  • app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt

@Lurux
Copy link
Copy Markdown
Author

Lurux commented Mar 24, 2026

@nyxiereal any thoughts on this so we can then move on with #3284 ?

@nyxiereal
Copy link
Copy Markdown
Member

this goes beyond what we would like to have in the project, the current implementation is fine. there is also a feature freeze rn, so no.

@nyxiereal nyxiereal closed this Apr 3, 2026
@Lurux
Copy link
Copy Markdown
Author

Lurux commented Apr 4, 2026

@nyxiereal was the feature freeze for the new release or is it something else ? May I ask you to take a glance at #3284 as it is based on this, should I rewrite it without this or drop the work there ? This would be necessary for the app to feel usable to me...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants