Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
455bcf1
Disable KtLint rules about trailing commas
rivaldi8 Nov 23, 2024
a17249f
Stop Ktlint from complaining about function names when annotated with…
rivaldi8 Nov 23, 2024
c24ae35
Fix genre context menu having no playlists on "Add to playlist" submenu
rivaldi8 Aug 26, 2025
2f1ceed
Implement minimal song list
rivaldi8 Feb 24, 2025
80fea9e
Add song context menu with "Add to queue" entry
rivaldi8 Mar 18, 2025
ff6dc70
Add "Add play next" context menu entry
rivaldi8 Mar 23, 2025
8d87702
Add "Song info" context menu entry
rivaldi8 Mar 23, 2025
ec65e6f
Add "Exclude" context menu entry
rivaldi8 Mar 23, 2025
0104819
Add "Edit tags" context menu entry
rivaldi8 Mar 23, 2025
c709df7
Add "Delete" context menu entry
rivaldi8 Mar 23, 2025
ace9180
Add "Add to playlist" context menu entry
rivaldi8 Mar 29, 2025
62b6b06
Play songs when clicked
rivaldi8 Apr 6, 2025
03b15e3
Add shuffle action item to the song list
rivaldi8 Apr 6, 2025
73b56ec
Implement song selection with text mark
rivaldi8 Apr 19, 2025
dcfaa60
Implement sorting
rivaldi8 Jul 7, 2025
6bae156
Remove no longer needed presenter
rivaldi8 Aug 11, 2025
b73b163
Move selection handing to a new toolbar helper for Compose
rivaldi8 Aug 17, 2025
20ff9cc
Add song thumbnails
rivaldi8 Aug 29, 2025
3369a28
Preload thumbnail images
rivaldi8 Sep 29, 2025
d4f8925
Implement proper selection mark
rivaldi8 Oct 26, 2025
f04703a
Pass missing modifier parameter to SongList
rivaldi8 Nov 8, 2025
954ed26
Extract condition to check if a song can be deleted into a new Song m…
rivaldi8 Nov 8, 2025
584830d
Make QueueManager.remove also hande Song to simplify and avoid code r…
rivaldi8 Nov 8, 2025
dc50ab9
Use immutable types for composable paramters when possible
rivaldi8 Nov 9, 2025
00503e0
Merge duplicated (songs/genres) AddToPlaylistSubmenu into a parametri…
rivaldi8 Nov 9, 2025
9bd0513
Add theme support
rivaldi8 Nov 9, 2025
68abbe6
Clean up SongListFragment
rivaldi8 Dec 19, 2025
7f68b15
Write unit tests for SongListViewModel
rivaldi8 Nov 29, 2025
9985bf6
Clean up PlaylistMenuPresenter
rivaldi8 Dec 20, 2025
2ba3220
Don't show scrollbar popup for certain sort orders
rivaldi8 Dec 21, 2025
4882dbc
Fix long text not fitting properly in the fast scroller's popup
rivaldi8 Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ ktlint_standard_filename=disabled
ktlint_standard_package-name=disabled
ktlint_code_style=android_studio
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_trailing-comma-on-declaration-site=disabled
ktlint_standard_trailing-comma-on-call-site=disabled
ktlint_function_naming_ignore_when_annotated_with=Composable
4 changes: 4 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ android {
// Excludes the support library because it's already included by Glide.
isTransitive = false
}
implementation(libs.glide.compose)

// About Libraries
implementation(libs.mikepenz.aboutlibrariesCore)
Expand All @@ -259,6 +260,9 @@ android {
implementation(libs.firebase.crashlytics)

// Testing
testImplementation(libs.kotest)
testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutinesTest)
androidTestImplementation(libs.androidx.runner)
androidTestImplementation(libs.androidx.rules)
androidTestImplementation(libs.androidx.core.ktx)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.simplecityapps.shuttle.ui.common

import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible
import com.simplecityapps.shuttle.model.MediaProviderType
import com.simplecityapps.shuttle.model.Song
import kotlin.collections.distinct
import kotlin.collections.map
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import timber.log.Timber

class ComposeContextualToolbarHelper {

var toolbar: Toolbar? = null
var contextualToolbar: Toolbar? = null

private val _selectedSongsState = MutableStateFlow(emptySet<Song>())
val selectedSongsState = _selectedSongsState.asStateFlow()
val selectedSongCountState = _selectedSongsState.asStateFlow()
.map { selectedSongs -> selectedSongs.size }

fun toggleSongSelection(song: Song) {
Timber.d("foo: toggleSongSelection: ${hashCode()}")
_selectedSongsState.value = if (_selectedSongsState.value.contains(song)) {
_selectedSongsState.value - song
} else {
_selectedSongsState.value + song
}
}

fun clearSelection() {
_selectedSongsState.value = emptySet()
}

fun isSelecting() = _selectedSongsState.value.isNotEmpty()

fun show() {
contextualToolbar?.let { contextualToolbar ->
toolbar?.isVisible = false
contextualToolbar.isVisible = true
contextualToolbar.setNavigationOnClickListener {
hide()
}
} ?: Timber.e("Failed to show contextual toolbar: toolbar null")
}

fun hide() {
toolbar?.isVisible = true
contextualToolbar?.isVisible = false
contextualToolbar?.setNavigationOnClickListener(null)
clearSelection()
}

fun selectedSongsMediaProviders(): List<MediaProviderType> = selectedSongsState
.value
.map { it.mediaProvider }
.distinct()
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ fun FastScroller(
thumb: @Composable () -> Unit = {
DefaultThumb()
},
popup: @Composable (index: Int) -> Unit = { currentItemIndex ->
DefaultPopup(text = getPopupText(currentItemIndex))
}
popup: @Composable ((index: Int) -> Unit)? = null
) {
val coroutineScope = rememberCoroutineScope()
val density = LocalDensity.current
Expand Down Expand Up @@ -175,6 +173,9 @@ fun FastScroller(
thumb()
}

val resolvedPopup = popup ?: { currentItemIndex ->
DefaultPopup(text = getPopupText(currentItemIndex))
}
// Position the popup so its bottom aligns with the thumb center.
var popupHeight by remember { mutableFloatStateOf(0f) }
Box(
Expand All @@ -193,7 +194,7 @@ fun FastScroller(
enter = fadeIn(tween(durationMillis = 150)),
exit = fadeOut(tween(durationMillis = 200))
) {
popup(currentItemIndex)
resolvedPopup(currentItemIndex)
}
}
}
Expand Down Expand Up @@ -238,7 +239,7 @@ fun DefaultPopup(
text?.let {
Box(
modifier = modifier
.padding(end = 16.dp)
.offset(x = (-16).dp)
.sizeIn(minWidth = 64.dp, minHeight = 64.dp)
.background(
color = MaterialTheme.colorScheme.primary,
Expand All @@ -248,7 +249,8 @@ fun DefaultPopup(
bottomStartPercent = 50,
bottomEndPercent = 0
)
),
)
.padding(start = 16.dp, end = 16.dp),
contentAlignment = Alignment.Center
) {
Text(
Expand All @@ -261,6 +263,10 @@ fun DefaultPopup(
}
}

/** Pass this to the popup composable in FastScroller to hide it. */
@Composable
fun NoPopup(index: Int) = Unit

/**
* Data class holding computed thumb scroll state, including an estimated average item height.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.simplecityapps.shuttle.ui.screens.library.genres
package com.simplecityapps.shuttle.ui.screens.library

import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
Expand All @@ -7,22 +7,22 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.simplecityapps.shuttle.R
import com.simplecityapps.shuttle.model.Genre
import com.simplecityapps.shuttle.model.Playlist
import com.simplecityapps.shuttle.ui.screens.playlistmenu.PlaylistData
import kotlinx.collections.immutable.ImmutableList

@Composable
fun AddToPlaylistSubmenu(
genre: Genre,
fun <T, R : PlaylistData> AddToPlaylistSubmenu(
playableItem: T,
playlists: ImmutableList<Playlist>,
onAddToPlaylist: (playlist: Playlist, playlistData: PlaylistData) -> Unit,
playlistDataCreator: (playableItem: T) -> R,
modifier: Modifier = Modifier,
expanded: Boolean = false,
onDismiss: () -> Unit = {},
onShowCreatePlaylistDialog: (genre: Genre) -> Unit
onShowCreatePlaylistDialog: (playableItem: T) -> Unit,
) {
val playlistData = PlaylistData.Genres(genre)
val playlistData = playlistDataCreator(playableItem)

DropdownMenu(
modifier = modifier,
Expand All @@ -32,7 +32,7 @@ fun AddToPlaylistSubmenu(
DropdownMenuItem(
text = { Text(stringResource(id = R.string.playlist_menu_create_playlist)) },
onClick = {
onShowCreatePlaylistDialog(genre)
onShowCreatePlaylistDialog(playableItem)
onDismiss()
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class GenreListFragment :

composeView.setContent {
val viewState by viewModel.viewState.collectAsState()
val playlists by playlistMenuPresenter.playlistsState.collectAsState()
val theme by viewModel.theme.collectAsStateWithLifecycle()
val accent by viewModel.accent.collectAsStateWithLifecycle()

Expand All @@ -72,7 +73,7 @@ class GenreListFragment :
) {
GenreList(
viewState = viewState,
playlists = playlistMenuPresenter.playlists.toImmutableList(),
playlists = playlists.toImmutableList(),
onSelectGenre = {
onGenreSelected(it)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
import com.simplecityapps.shuttle.R
import com.simplecityapps.shuttle.model.Genre
import com.simplecityapps.shuttle.model.Playlist
import com.simplecityapps.shuttle.ui.screens.library.AddToPlaylistSubmenu
import com.simplecityapps.shuttle.ui.screens.playlistmenu.PlaylistData
import kotlinx.collections.immutable.ImmutableList

Expand Down Expand Up @@ -108,11 +109,12 @@ fun GenreMenu(
}
}
AddToPlaylistSubmenu(
genre = genre,
playableItem = genre,
expanded = isAddToPlaylistSubmenuOpen,
onDismiss = { isAddToPlaylistSubmenuOpen = false },
playlists = playlists,
onAddToPlaylist = onAddToPlaylist,
playlistDataCreator = { genre -> PlaylistData.Genres(genre) },
onShowCreatePlaylistDialog = onShowCreatePlaylistDialog
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.simplecityapps.shuttle.ui.screens.library.songs

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.simplecityapps.shuttle.R
import com.simplecityapps.shuttle.persistence.GeneralPreferenceManager
import com.simplecityapps.shuttle.ui.theme.AppTheme

@Composable
fun ShuffleListItem(
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
) {
Row(
modifier = modifier
.padding(all = 8.dp)
.clickable { onClick() },
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(R.drawable.ic_shuffle_black_24dp),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp),
text = stringResource(com.simplecityapps.shuttle.R.string.btn_shuffle),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
)
}
}

@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun ShuffleListItemPreview() {
AppTheme(
accent = GeneralPreferenceManager.Accent.Default
) {
ShuffleListItem()
}
}
Loading