diff --git a/.editorconfig b/.editorconfig index 5facbdf..fb68063 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,2 +1,5 @@ [*.{kt,kts}] ktlint_function_naming_ignore_when_annotated_with=Composable + +[lib/**] +ktlint=disabled diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..828eb23 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "lib/hotwire-native-android"] + path = lib/hotwire-native-android + url = https://github.com/aidewoode/hotwire-native-android.git + branch = feat/extend_navigtion_bar diff --git a/androidApp/src/main/java/org/blackcandy/android/MainActivity.kt b/androidApp/src/main/java/org/blackcandy/android/MainActivity.kt index 94914bf..d6360c8 100644 --- a/androidApp/src/main/java/org/blackcandy/android/MainActivity.kt +++ b/androidApp/src/main/java/org/blackcandy/android/MainActivity.kt @@ -19,12 +19,12 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.accompanist.themeadapter.material3.Mdc3Theme -import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.navigation.NavigationBarView import dev.hotwire.navigation.activities.HotwireActivity import dev.hotwire.navigation.navigator.NavigatorConfiguration -import dev.hotwire.navigation.tabs.HotwireBottomNavigationController -import dev.hotwire.navigation.tabs.HotwireBottomTab +import dev.hotwire.navigation.tabs.HotwireNavigationController +import dev.hotwire.navigation.tabs.HotwireTab import dev.hotwire.navigation.tabs.navigatorConfigurations import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -37,7 +37,7 @@ import org.blackcandy.shared.viewmodels.MusicServiceViewModel import org.koin.androidx.viewmodel.ext.android.viewModel class MainActivity : HotwireActivity() { - private lateinit var bottomNavigationController: HotwireBottomNavigationController + private lateinit var navigationController: HotwireNavigationController private val viewModel: MainViewModel by viewModel() @@ -45,7 +45,7 @@ class MainActivity : HotwireActivity() { private lateinit var binding: ActivityMainBinding private lateinit var playerBottomSheetBehavior: BottomSheetBehavior - private lateinit var mainTabs: List + private lateinit var mainTabs: List private val playerBottomSheetCallback by lazy { object : BottomSheetBehavior.BottomSheetCallback() { @@ -242,11 +242,12 @@ class MainActivity : HotwireActivity() { } private fun setupBottomTabs() { - val bottomNavigationView = findViewById(R.id.bottom_nav) + val navigationView: NavigationBarView = + findViewById(R.id.bottom_nav) ?: findViewById(R.id.rail_nav) - bottomNavigationController = HotwireBottomNavigationController(this, bottomNavigationView) - bottomNavigationController.load(mainTabs, viewModel.selectedTabIndex) - bottomNavigationController.setOnTabSelectedListener { index, _ -> + navigationController = HotwireNavigationController(this, navigationView) + navigationController.load(mainTabs, viewModel.selectedTabIndex) + navigationController.setOnTabSelectedListener { index, _ -> viewModel.selectedTabIndex = index } } diff --git a/androidApp/src/main/java/org/blackcandy/android/MainTabs.kt b/androidApp/src/main/java/org/blackcandy/android/MainTabs.kt index 22d60df..1b67ea5 100644 --- a/androidApp/src/main/java/org/blackcandy/android/MainTabs.kt +++ b/androidApp/src/main/java/org/blackcandy/android/MainTabs.kt @@ -1,11 +1,11 @@ package org.blackcandy.android import dev.hotwire.navigation.navigator.NavigatorConfiguration -import dev.hotwire.navigation.tabs.HotwireBottomTab +import dev.hotwire.navigation.tabs.HotwireTab -fun buildMainTabs(serverAddress: String): List = +fun buildMainTabs(serverAddress: String): List = listOf( - HotwireBottomTab( + HotwireTab( title = "Home", iconResId = R.drawable.baseline_home_24, configuration = @@ -15,7 +15,7 @@ fun buildMainTabs(serverAddress: String): List = navigatorHostId = R.id.home_container, ), ), - HotwireBottomTab( + HotwireTab( title = "Library", iconResId = R.drawable.baseline_library_music_24, configuration = diff --git a/androidApp/src/main/java/org/blackcandy/android/compose/player/FullPlayer.kt b/androidApp/src/main/java/org/blackcandy/android/compose/player/FullPlayer.kt index 9805bcb..83ea798 100644 --- a/androidApp/src/main/java/org/blackcandy/android/compose/player/FullPlayer.kt +++ b/androidApp/src/main/java/org/blackcandy/android/compose/player/FullPlayer.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,6 +21,8 @@ import org.blackcandy.shared.models.Song fun FullPlayer( modifier: Modifier = Modifier, windowSizeClass: WindowSizeClass, + inWideLayout: Boolean = false, + inCompactHeight: Boolean = false, currentSong: Song?, isPlaying: Boolean, isLoading: Boolean, @@ -34,12 +35,9 @@ fun FullPlayer( onSeek: (Double) -> Unit, onModeSwitchButtonClicked: () -> Unit, onFavoriteButtonClicked: () -> Unit, - onPlaylistButtonClicked: () -> Unit, + onPlaylistButtonClicked: (() -> Unit)? = null, ) { - if ( - windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact && - windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact - ) { + if (inCompactHeight) { PlayerHorizontalLayout( modifier = modifier, currentSong = currentSong, @@ -59,6 +57,7 @@ fun FullPlayer( } else { PlayerVerticalLayout( modifier = modifier, + inWideLayout = inWideLayout, currentSong = currentSong, isPlaying = isPlaying, isLoading = isLoading, @@ -92,7 +91,7 @@ fun PlayerHorizontalLayout( onSeek: (Double) -> Unit, onModeSwitchButtonClicked: () -> Unit, onFavoriteButtonClicked: () -> Unit, - onPlaylistButtonClicked: () -> Unit, + onPlaylistButtonClicked: (() -> Unit)? = null, ) { Row( modifier = @@ -153,6 +152,7 @@ fun PlayerHorizontalLayout( @Composable fun PlayerVerticalLayout( modifier: Modifier, + inWideLayout: Boolean, currentSong: Song?, isPlaying: Boolean, isLoading: Boolean, @@ -165,7 +165,7 @@ fun PlayerVerticalLayout( onSeek: (Double) -> Unit, onModeSwitchButtonClicked: () -> Unit, onFavoriteButtonClicked: () -> Unit, - onPlaylistButtonClicked: () -> Unit, + onPlaylistButtonClicked: (() -> Unit)? = null, isExpandedHeight: Boolean, ) { Column( @@ -175,7 +175,9 @@ fun PlayerVerticalLayout( .fillMaxWidth() .padding(horizontal = dimensionResource(R.dimen.padding_small)), ) { - Spacer(modifier = Modifier.weight(1f)) + if (!inWideLayout) { + Spacer(modifier = Modifier.weight(1f)) + } Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -195,7 +197,7 @@ fun PlayerVerticalLayout( modifier = Modifier .widthIn(max = dimensionResource(R.dimen.player_content_max_width)) - .padding(top = dimensionResource(R.dimen.padding_small)), + .padding(vertical = dimensionResource(R.dimen.padding_small)), isPlaying = isPlaying, isLoading = isLoading, largeIcon = true, @@ -210,7 +212,9 @@ fun PlayerVerticalLayout( ) } - Spacer(modifier = Modifier.weight(1f)) + if (!inWideLayout) { + Spacer(modifier = Modifier.weight(1f)) + } PlayerActions( modifier = diff --git a/androidApp/src/main/java/org/blackcandy/android/compose/player/PlayerActions.kt b/androidApp/src/main/java/org/blackcandy/android/compose/player/PlayerActions.kt index d7f9c8d..3908fdd 100644 --- a/androidApp/src/main/java/org/blackcandy/android/compose/player/PlayerActions.kt +++ b/androidApp/src/main/java/org/blackcandy/android/compose/player/PlayerActions.kt @@ -21,7 +21,7 @@ fun PlayerActions( isFavorited: Boolean, onModeSwitchButtonClicked: () -> Unit, onFavoriteButtonClicked: () -> Unit, - onPlaylistButtonClicked: () -> Unit, + onPlaylistButtonClicked: (() -> Unit)? = null, ) { Row( horizontalArrangement = Arrangement.SpaceBetween, @@ -53,13 +53,15 @@ fun PlayerActions( } } - IconButton( - onClick = onPlaylistButtonClicked, - ) { - Icon( - painter = painterResource(R.drawable.baseline_format_list_bulleted_24), - contentDescription = stringResource(R.string.playlist), - ) + if (onPlaylistButtonClicked != null) { + IconButton( + onClick = onPlaylistButtonClicked, + ) { + Icon( + painter = painterResource(R.drawable.baseline_format_list_bulleted_24), + contentDescription = stringResource(R.string.playlist), + ) + } } } } diff --git a/androidApp/src/main/java/org/blackcandy/android/compose/player/PlayerScreen.kt b/androidApp/src/main/java/org/blackcandy/android/compose/player/PlayerScreen.kt index 7168480..10078e9 100644 --- a/androidApp/src/main/java/org/blackcandy/android/compose/player/PlayerScreen.kt +++ b/androidApp/src/main/java/org/blackcandy/android/compose/player/PlayerScreen.kt @@ -1,6 +1,10 @@ package org.blackcandy.android.compose.player +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -9,23 +13,30 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -49,6 +60,106 @@ fun PlayerScreen( windowSizeClass: WindowSizeClass, ) { val uiState by viewModel.uiState.collectAsState() + val isWideLayout = + windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact && + windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact + + if (isWideLayout) { + PlayerScreenWideLayout( + snackbarHostState = snackbarHostState, + viewModel = viewModel, + windowSizeClass = windowSizeClass, + uiState = uiState, + ) + } else { + PlayerScreenCompactLayout( + navController = navController, + snackbarHostState = snackbarHostState, + viewModel = viewModel, + windowSizeClass = windowSizeClass, + uiState = uiState, + ) + } +} + +@Composable +fun PlayerScreenWideLayout( + snackbarHostState: SnackbarHostState, + viewModel: PlayerViewModel, + windowSizeClass: WindowSizeClass, + uiState: org.blackcandy.shared.viewmodels.PlayerUiState, +) { + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + containerColor = Color.Transparent, + ) { innerPadding -> + Row( + modifier = + Modifier + .padding(innerPadding) + .fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium)), + ) { + FullPlayer( + modifier = + Modifier + .weight(1f) + .verticalScroll(rememberScrollState()), + windowSizeClass = windowSizeClass, + inWideLayout = true, + currentSong = uiState.musicState.currentSong, + isPlaying = uiState.musicState.isPlaying, + isLoading = uiState.musicState.isLoading, + currentPosition = uiState.currentPosition, + playbackMode = uiState.musicState.playbackMode, + onPreviousButtonClicked = { viewModel.previous() }, + onNextButtonClicked = { viewModel.next() }, + onPlayButtonClicked = { viewModel.play() }, + onPauseButtonClicked = { viewModel.pause() }, + onSeek = { viewModel.seekTo(it) }, + onModeSwitchButtonClicked = { viewModel.nextMode() }, + onFavoriteButtonClicked = { viewModel.toggleFavorite() }, + onPlaylistButtonClicked = null, + ) + + Column( + modifier = Modifier.weight(1f), + ) { + PlaylistHeader( + tracksCount = uiState.musicState.playlist.size, + onClearAllButtonClicked = { viewModel.clearPlaylist() }, + ) + + Playlist( + modifier = + Modifier + .heightIn(max = dimensionResource(R.dimen.playlist_max_height)), + playlist = uiState.musicState.playlist, + currentSong = uiState.musicState.currentSong, + onItemClicked = { songId -> viewModel.playOn(songId) }, + onItemSweepToDismiss = { songId -> viewModel.removeSongFromPlaylist(songId) }, + onItemMoved = { from, to -> viewModel.moveSongInPlaylist(from, to) }, + ) + } + } + + uiState.alertMessage?.let { alertMessage -> + ShowSnackbar(alertMessage, snackbarHostState) { + viewModel.alertMessageShown() + } + } + } +} + +@Composable +fun PlayerScreenCompactLayout( + navController: NavHostController, + snackbarHostState: SnackbarHostState, + viewModel: PlayerViewModel, + windowSizeClass: WindowSizeClass, + uiState: org.blackcandy.shared.viewmodels.PlayerUiState, +) { val backStackEntry by navController.currentBackStackEntryAsState() val currentRoute = @@ -84,6 +195,9 @@ fun PlayerScreen( .fillMaxHeight() .verticalScroll(rememberScrollState()), windowSizeClass = windowSizeClass, + inCompactHeight = + windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact && + windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact, currentSong = uiState.musicState.currentSong, isPlaying = uiState.musicState.isPlaying, isLoading = uiState.musicState.isLoading, @@ -122,6 +236,30 @@ fun PlayerScreen( } } +@Composable +fun PlaylistHeader( + tracksCount: Int, + onClearAllButtonClicked: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 16.dp, end = 4.dp), + ) { + Text( + text = pluralStringResource(R.plurals.tracks_count, tracksCount, tracksCount), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.weight(1f), + ) + + IconButton(onClick = onClearAllButtonClicked) { + Icon( + painter = painterResource(R.drawable.baseline_clear_all_24), + contentDescription = stringResource(R.string.clear_all), + ) + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun PlaylistAppBar( diff --git a/androidApp/src/main/java/org/blackcandy/android/compose/player/PlaylistItem.kt b/androidApp/src/main/java/org/blackcandy/android/compose/player/PlaylistItem.kt index f50daf3..5fae9f4 100644 --- a/androidApp/src/main/java/org/blackcandy/android/compose/player/PlaylistItem.kt +++ b/androidApp/src/main/java/org/blackcandy/android/compose/player/PlaylistItem.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxValue @@ -109,6 +110,7 @@ fun PlaylistItem( contentDescription = stringResource(R.string.drag_handle), ) }, + colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.inverseOnSurface), ) } diff --git a/androidApp/src/main/res/layout-sw600dp-land/activity_main.xml b/androidApp/src/main/res/layout-sw600dp-land/activity_main.xml deleted file mode 100644 index 40fdf18..0000000 --- a/androidApp/src/main/res/layout-sw600dp-land/activity_main.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - diff --git a/androidApp/src/main/res/layout-w600dp/activity_main.xml b/androidApp/src/main/res/layout-w600dp/activity_main.xml index 37fbee6..c81fb2a 100644 --- a/androidApp/src/main/res/layout-w600dp/activity_main.xml +++ b/androidApp/src/main/res/layout-w600dp/activity_main.xml @@ -25,7 +25,7 @@ diff --git a/androidApp/src/main/res/values/dimens.xml b/androidApp/src/main/res/values/dimens.xml index d893172..97d38e7 100644 --- a/androidApp/src/main/res/values/dimens.xml +++ b/androidApp/src/main/res/values/dimens.xml @@ -17,4 +17,5 @@ 340dp 400dp 500dp + 550dp \ No newline at end of file diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index 519af4e..6bd0d98 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -38,4 +38,8 @@ Nothing to play here Drag Handle Added to playlist + + %d track + %d tracks + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5728eb..9de2f66 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ ktor = "2.3.4" media3 = "1.3.1" reorderable = "1.3.3" kotlin = "2.2.0" -androidGradlePlugin = "8.13.1" +androidGradlePlugin = "8.13.2" kotlinStdlib = "2.2.0" kotlinTest = "2.2.0" runner = "1.5.2" diff --git a/lib/hotwire-native-android b/lib/hotwire-native-android new file mode 160000 index 0000000..2b65834 --- /dev/null +++ b/lib/hotwire-native-android @@ -0,0 +1 @@ +Subproject commit 2b658346512fc0e9cda6b0bf3b3ebf9c8a91bd1a diff --git a/settings.gradle.kts b/settings.gradle.kts index de2afa6..059aff1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,13 @@ dependencyResolutionManagement { mavenCentral() } } +includeBuild("lib/hotwire-native-android") { + dependencySubstitution { + substitute(module("dev.hotwire:core")).using(project(":core")) + substitute(module("dev.hotwire:navigation-fragments")).using(project(":navigation-fragments")) + } +} + rootProject.name = "BlackCandy" include(":androidApp") include(":shared")