Skip to content

Web Search Dialog Compose#256

Open
TheTerminatorOfProgramming wants to merge 1 commit intomardous:masterfrom
TheTerminatorOfProgramming:web_search_dialog_compose
Open

Web Search Dialog Compose#256
TheTerminatorOfProgramming wants to merge 1 commit intomardous:masterfrom
TheTerminatorOfProgramming:web_search_dialog_compose

Conversation

@TheTerminatorOfProgramming
Copy link
Contributor

@TheTerminatorOfProgramming TheTerminatorOfProgramming commented Jan 11, 2026

Convert the Web Search Dialog to Compose

Summary by Sourcery

Convert the web search dialog to a Compose-based bottom sheet fragment and update navigation to use it instead of the legacy dialog.

New Features:

  • Add a Compose-based WebSearchBottomSheet UI for performing web searches on the current song.
  • Introduce WebSearchFragment as a BottomSheetDialogFragment hosting the Compose web search UI and receiving the selected song via navigation arguments.

Enhancements:

  • Wire the now playing WebSearch action to navigate to the new WebSearchFragment using navigation arguments.
  • Extend the navigation graph with a nav_web_search dialog destination and add a dedicated bottom sheet height dimension for the new UI.

Convert the Web Search Dialog to Compose
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 11, 2026

Reviewer's Guide

Replaces the legacy WebSearchDialog with a new Jetpack Compose-based bottom sheet dialog wired through Navigation, including a new nav graph dialog destination, a Compose screen and fragment wrapper, and an updated player action to launch it.

Sequence diagram for launching the new Compose web search bottom sheet

sequenceDiagram
    actor User
    participant AbsPlayerFragment
    participant NavController
    participant WebSearchFragment
    participant ComposeWebSearchBottomSheet
    participant Browser

    User->>AbsPlayerFragment: Tap NowPlayingAction.WebSearch
    AbsPlayerFragment->>NavController: navigate(nav_web_search, songDetailArgs(currentSong))
    NavController->>WebSearchFragment: Show dialog destination nav_web_search
    WebSearchFragment->>WebSearchFragment: Configure BottomSheetDialog behavior
    WebSearchFragment->>ComposeWebSearchBottomSheet: Set Compose content with song and context

    User->>ComposeWebSearchBottomSheet: Tap search engine button
    ComposeWebSearchBottomSheet->>Browser: context.startActivity(song.searchQuery(engine).openWeb().toChooser())
    Browser-->>User: Open web search results
Loading

Updated class diagram for AbsPlayerFragment and the new WebSearchFragment Compose screen

classDiagram
    class AbsPlayerFragment {
        <<abstract>>
        +onOptionsItemSelected(item)
        +handleNowPlayingAction(action)
    }

    class WebSearchFragment {
        +navArgs : WebSearchFragmentArgs
        +song : Song
        +onCreateView(inflater, container, savedInstanceState) View
    }

    class WebSearchFragmentArgs {
        +extraSong : Song
    }

    class WebSearchBottomSheetComposable {
        <<Composable>>
        +WebSearchBottomSheet(song : Song, context : Context) void
    }

    class WebSearchEngine {
        <<enum>>
        +entries : Array~WebSearchEngine~
    }

    class Song

    class BottomSheetDialogFragment

    class BottomSheetDialog {
        +behavior
    }

    class BottomSheetBehavior {
        +isFitToContents : Boolean
        +skipCollapsed : Boolean
        +peekHeight : Int
        +maxHeight : Int
    }

    class BottomSheetDialogSurfaceComposable {
        <<Composable>>
        +BottomSheetDialogSurface(content) void
    }

    AbsPlayerFragment ..> WebSearchFragment : navigate nav_web_search
    WebSearchFragment --|> BottomSheetDialogFragment
    WebSearchFragment *-- WebSearchFragmentArgs
    WebSearchFragment --> Song
    WebSearchFragment ..> WebSearchBottomSheetComposable : setContent
    WebSearchFragment ..> BottomSheetDialog
    BottomSheetDialog *-- BottomSheetBehavior

    WebSearchBottomSheetComposable --> Song
    WebSearchBottomSheetComposable ..> WebSearchEngine : use entries
    WebSearchBottomSheetComposable ..> BottomSheetDialogSurfaceComposable

    WebSearchEngine <.. Song : searchQuery extension
Loading

File-Level Changes

Change Details Files
Wire web search into the navigation graph instead of showing the legacy dialog directly.
  • Add a new dialog destination nav_web_search with a Song argument extraSong to the main navigation graph.
  • Update the now playing WebSearch action to navigate to the new destination using songDetailArgs instead of instantiating WebSearchDialog directly.
app/src/main/res/navigation/graph_main.xml
app/src/main/java/com/mardous/booming/ui/component/base/AbsPlayerFragment.kt
Implement a Compose-based web search bottom sheet UI and host it in a BottomSheetDialogFragment.
  • Create WebSearchBottomSheet composable that renders a title and four buttons mapped to WebSearchEngine entries, launching the appropriate web search intent for the current song.
  • Introduce WebSearchFragment as a BottomSheetDialogFragment that configures bottom sheet behavior (fitToContents, skipCollapsed, peekHeight/maxHeight via shuffle_height) and hosts the Compose content within BoomingMusicTheme.
  • Add a new dimension shuffle_height used to control the bottom sheet peek height and max height.
app/src/main/java/com/mardous/booming/ui/screen/websearch/WebSearchScreen.kt
app/src/main/java/com/mardous/booming/ui/screen/websearch/WebSearchFragment.kt
app/src/main/res/values/dimens.xml
Remove the old XML-based WebSearchDialog implementation in favor of the new Compose flow.
  • Delete the legacy WebSearchDialog class and its usages, fully migrating web search to the new composable-based bottom sheet.
app/src/main/java/com/mardous/booming/ui/dialogs/WebSearchDialog.kt
app/src/main/java/com/mardous/booming/ui/component/base/AbsPlayerFragment.kt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • In WebSearchBottomSheet the song parameter is nullable but is always accessed with song!!; either make song non-nullable or add an early-return/disabled state to avoid potential NPEs.
  • The four Button blocks in WebSearchBottomSheet are nearly identical and indexed manually into engines; consider iterating over engines (or WebSearchEngine.entries) to generate buttons dynamically so the UI stays in sync if the enum changes.
  • Instead of passing an explicit Context into WebSearchBottomSheet, consider using LocalContext.current inside the composable to keep the API more idiomatic for Compose and reduce coupling to the caller.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `WebSearchBottomSheet` the `song` parameter is nullable but is always accessed with `song!!`; either make `song` non-nullable or add an early-return/disabled state to avoid potential NPEs.
- The four `Button` blocks in `WebSearchBottomSheet` are nearly identical and indexed manually into `engines`; consider iterating over `engines` (or `WebSearchEngine.entries`) to generate buttons dynamically so the UI stays in sync if the enum changes.
- Instead of passing an explicit `Context` into `WebSearchBottomSheet`, consider using `LocalContext.current` inside the composable to keep the API more idiomatic for Compose and reduce coupling to the caller.

## Individual Comments

### Comment 1
<location> `app/src/main/java/com/mardous/booming/ui/screen/websearch/WebSearchScreen.kt:35-39` </location>
<code_context>
+import com.mardous.booming.ui.component.compose.BottomSheetDialogSurface
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun WebSearchBottomSheet(
+    song: Song?,
+    context: Context
+) {
+    val engines = WebSearchEngine.entries.toTypedArray()
+
+    BottomSheetDialogSurface {
</code_context>

<issue_to_address>
**issue (bug_risk):** Nullable `song` combined with `song!!` dereferences introduces a crash risk.

`song` is nullable (`Song?`), but the button handlers use `song!!`, so this will crash if the sheet is ever opened without a song (e.g., missing nav args or future reuse). Either require a non-null `Song` in the function signature and callers, or add null handling (early return / disabled actions) and remove the `!!` usage.
</issue_to_address>

### Comment 2
<location> `app/src/main/java/com/mardous/booming/ui/screen/websearch/WebSearchScreen.kt:39-48` </location>
<code_context>
-    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-        val song = BundleCompat.getParcelable(requireArguments(), EXTRA_SONG, Song::class.java)!!
-
-        val engines = WebSearchEngine.entries.toTypedArray()
-        val titles = engines.map { getString(it.nameRes) }.toTypedArray()
-
</code_context>

<issue_to_address>
**suggestion:** Hard-coded enum indices and duplicated button code make the implementation fragile and harder to maintain.

The four buttons are manually duplicated and tied to `engines[0..3]` / `WebSearchEngine.entries[0..3]`, which will break if the enum size or order changes and is easy to miss when updating. Instead, iterate over `engines` (or `WebSearchEngine.entries`) to generate the buttons dynamically, using the enum values directly to avoid magic indices and duplication.

Suggested implementation:

```
import android.content.Context
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.mardous.booming.extensions.media.searchQuery
import com.mardous.booming.extensions.openWeb
import com.mardous.booming.extensions.toChooser
import com.mardous.booming.ui.component.compose.BottomSheetDialogSurface
import com.mardous.booming.ui.screen.websearch.WebSearchEngine

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WebSearchBottomSheet(
    song: Song?,
    context: Context
) {
    val query = song?.searchQuery ?: return
    val engines = remember { WebSearchEngine.entries.toList() }
    val interactionSource = remember { MutableInteractionSource() }

    BottomSheetDialogSurface {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight()
                .verticalScroll(rememberScrollState())
                .padding(horizontal = 16.dp, vertical = 24.dp),
            verticalArrangement = Arrangement.spacedBy(12.dp)
        ) {
            engines.forEach { engine ->
                Button(
                    modifier = Modifier.fillMaxWidth(),
                    onClick = {
                        // This assumes a WebSearchEngine API similar to:
                        // fun createUri(query: String): Uri
                        // Adjust to match the actual enum implementation.
                        val intent = engine
                            .createUri(query)
                            .toChooser(context)
                        context.openWeb(intent)
                    },
                    interactionSource = interactionSource
                ) {
                    Text(text = stringResource(id = engine.nameRes))
                }
            }

            Spacer(modifier = Modifier.height(8.dp))
        }
    }
}

```

I had to reconstruct the body of `WebSearchBottomSheet` based on your comment and the imports, so you will need to:

1. Replace the `onClick` block body with the exact logic you currently have for launching a web search, but using `engine` instead of a hard‑coded `engines[0]` / `WebSearchEngine.entries[0]` etc. For example, if your existing code is:

   - `context.openWeb(WebSearchEngine.entries[0].createUri(query).toChooser(context))`

   then inside the loop, change it to:

   - `context.openWeb(engine.createUri(query).toChooser(context))`

2. Remove any now‑unused imports or parameters that may have been present in the original implementation (e.g. if you were using different button types or layout modifiers).
3. If your design system uses a custom button composable instead of `Button` / `Text`, swap those in while keeping the `engines.forEach { engine -> ... }` pattern.
4. Ensure `WebSearchEngine` is imported from the correct package; if it’s already in the same package you can remove the explicit import I added.
</issue_to_address>

### Comment 3
<location> `app/src/main/res/values/dimens.xml:22` </location>
<code_context>
     <dimen name="mini_player_height">70dp</dimen>
     <dimen name="bottom_nav_height">80dp</dimen>
     <dimen name="queue_height">400dp</dimen>
+    <dimen name="shuffle_height">800dp</dimen>

     <dimen name="icon_notification_size">32dp</dimen>
</code_context>

<issue_to_address>
**suggestion:** The hardcoded `800dp` bottom sheet height may not scale well across devices.

Using a fixed 800dp for `shuffle_height` (as the bottom sheet peek/max height) risks exceeding the viewport on small screens and looking disproportionate on large ones. Consider deriving this from the available screen height (or using `MATCH_PARENT`/percent-based behavior) instead of a large hardcoded dp value to keep the layout responsive across devices.

Suggested implementation:

```
    <dimen name="queue_height">400dp</dimen>
    <!-- Actual shuffle bottom sheet height is calculated at runtime based on the screen height.
         This value is only a fallback/default and should remain small. -->
    <dimen name="shuffle_height">0dp</dimen>

    <dimen name="icon_notification_size">32dp</dimen>

```

To fully implement the suggestion (derive the height from the available screen height), you will also need to:
1. Locate where `R.dimen.shuffle_height` is used (likely in the shuffle bottom sheet layout or where `BottomSheetBehavior` is configured).
2. Replace the direct use of the dimen as a fixed height with runtime logic, for example:
   - Obtain the screen height via `Resources.getSystem().displayMetrics.heightPixels` or a `WindowMetrics` API.
   - Compute a proportional height, e.g. `val shuffleHeight = (screenHeight * 0.6f).toInt()`.
   - Apply this height to the bottom sheet’s layout params or to `BottomSheetBehavior.peekHeight`.
3. Keep `shuffle_height` as a fallback (0dp or a very small value) and for previews, but rely on the runtime calculation for actual devices so the sheet scales appropriately on small and large screens.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +39 to +48
val engines = WebSearchEngine.entries.toTypedArray()

BottomSheetDialogSurface {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Column(
modifier = Modifier
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Hard-coded enum indices and duplicated button code make the implementation fragile and harder to maintain.

The four buttons are manually duplicated and tied to engines[0..3] / WebSearchEngine.entries[0..3], which will break if the enum size or order changes and is easy to miss when updating. Instead, iterate over engines (or WebSearchEngine.entries) to generate the buttons dynamically, using the enum values directly to avoid magic indices and duplication.

Suggested implementation:

import android.content.Context
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.mardous.booming.extensions.media.searchQuery
import com.mardous.booming.extensions.openWeb
import com.mardous.booming.extensions.toChooser
import com.mardous.booming.ui.component.compose.BottomSheetDialogSurface
import com.mardous.booming.ui.screen.websearch.WebSearchEngine

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WebSearchBottomSheet(
    song: Song?,
    context: Context
) {
    val query = song?.searchQuery ?: return
    val engines = remember { WebSearchEngine.entries.toList() }
    val interactionSource = remember { MutableInteractionSource() }

    BottomSheetDialogSurface {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight()
                .verticalScroll(rememberScrollState())
                .padding(horizontal = 16.dp, vertical = 24.dp),
            verticalArrangement = Arrangement.spacedBy(12.dp)
        ) {
            engines.forEach { engine ->
                Button(
                    modifier = Modifier.fillMaxWidth(),
                    onClick = {
                        // This assumes a WebSearchEngine API similar to:
                        // fun createUri(query: String): Uri
                        // Adjust to match the actual enum implementation.
                        val intent = engine
                            .createUri(query)
                            .toChooser(context)
                        context.openWeb(intent)
                    },
                    interactionSource = interactionSource
                ) {
                    Text(text = stringResource(id = engine.nameRes))
                }
            }

            Spacer(modifier = Modifier.height(8.dp))
        }
    }
}

I had to reconstruct the body of WebSearchBottomSheet based on your comment and the imports, so you will need to:

  1. Replace the onClick block body with the exact logic you currently have for launching a web search, but using engine instead of a hard‑coded engines[0] / WebSearchEngine.entries[0] etc. For example, if your existing code is:

    • context.openWeb(WebSearchEngine.entries[0].createUri(query).toChooser(context))

    then inside the loop, change it to:

    • context.openWeb(engine.createUri(query).toChooser(context))
  2. Remove any now‑unused imports or parameters that may have been present in the original implementation (e.g. if you were using different button types or layout modifiers).

  3. If your design system uses a custom button composable instead of Button / Text, swap those in while keeping the engines.forEach { engine -> ... } pattern.

  4. Ensure WebSearchEngine is imported from the correct package; if it’s already in the same package you can remove the explicit import I added.

<dimen name="mini_player_height">70dp</dimen>
<dimen name="bottom_nav_height">80dp</dimen>
<dimen name="queue_height">400dp</dimen>
<dimen name="shuffle_height">800dp</dimen>
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: The hardcoded 800dp bottom sheet height may not scale well across devices.

Using a fixed 800dp for shuffle_height (as the bottom sheet peek/max height) risks exceeding the viewport on small screens and looking disproportionate on large ones. Consider deriving this from the available screen height (or using MATCH_PARENT/percent-based behavior) instead of a large hardcoded dp value to keep the layout responsive across devices.

Suggested implementation:

    <dimen name="queue_height">400dp</dimen>
    <!-- Actual shuffle bottom sheet height is calculated at runtime based on the screen height.
         This value is only a fallback/default and should remain small. -->
    <dimen name="shuffle_height">0dp</dimen>

    <dimen name="icon_notification_size">32dp</dimen>

To fully implement the suggestion (derive the height from the available screen height), you will also need to:

  1. Locate where R.dimen.shuffle_height is used (likely in the shuffle bottom sheet layout or where BottomSheetBehavior is configured).
  2. Replace the direct use of the dimen as a fixed height with runtime logic, for example:
    • Obtain the screen height via Resources.getSystem().displayMetrics.heightPixels or a WindowMetrics API.
    • Compute a proportional height, e.g. val shuffleHeight = (screenHeight * 0.6f).toInt().
    • Apply this height to the bottom sheet’s layout params or to BottomSheetBehavior.peekHeight.
  3. Keep shuffle_height as a fallback (0dp or a very small value) and for previews, but rely on the runtime calculation for actual devices so the sheet scales appropriately on small and large screens.

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.

1 participant