From 1ba9f479d90303a8cf48938dbf12a2f56d315366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 11 Mar 2023 02:23:10 +0100 Subject: [PATCH 01/42] Started adding Spotify API impl and fixed crashes --- app/build.gradle.kts | 6 ++++ .../spotify_api/SpotifyApiRequests.kt | 29 +++++++++++++++++++ .../spowlo/ui/components/BottomDrawer.kt | 9 ++++-- .../ui/dialogs/DownloaderSettingsDialog.kt | 5 +--- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 14 ++++++--- .../ui/pages/downloader/DownloaderPage.kt | 6 ++-- app/src/main/res/values/strings.xml | 1 + color/build.gradle.kts | 2 +- gradle.properties | 4 ++- gradle/libs.versions.toml | 4 +-- 10 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 95fe5fb9..db53f9dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -112,10 +112,16 @@ android { ) if (keystorePropertiesFile.exists()) signingConfig = signingConfigs.getByName("debug") + + //add client id and secret to build config + buildConfigField("String", "CLIENT_ID", "\"${project.properties["CLIENT_ID"]}\"") + buildConfigField("String", "CLIENT_SECRET", "\"${project.properties["CLIENT_SECRET"]}\"") } debug { if (keystorePropertiesFile.exists()) signingConfig = signingConfigs.getByName("debug") + buildConfigField("String", "CLIENT_ID", "\"${project.properties["CLIENT_ID"]}\"") + buildConfigField("String", "CLIENT_SECRET", "\"${project.properties["CLIENT_SECRET"]}\"") } } compileOptions { diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt new file mode 100644 index 00000000..e5afe9af --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt @@ -0,0 +1,29 @@ +package com.bobbyesp.spowlo.features.spotify_api + +import com.adamratzman.spotify.SpotifyAppApi +import com.adamratzman.spotify.models.SpotifySearchResult +import com.adamratzman.spotify.spotifyAppApi +import com.adamratzman.spotify.utils.Market +import com.bobbyesp.spowlo.BuildConfig + +class SpotifyApiRequests { + + private val clientId = BuildConfig.CLIENT_ID + private val clientSecret = BuildConfig.CLIENT_SECRET + + private var api: SpotifyAppApi? = null + + suspend fun buildApi(): SpotifyAppApi { + if (api == null) { + api = spotifyAppApi( + clientId = clientId, + clientSecret = clientSecret + ).build() + } + return api!! + } + + suspend fun searchForTrack(query: String): SpotifySearchResult { + return api!!.search.search(query, limit = 50, offset = 1, market = Market.ES) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/BottomDrawer.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/BottomDrawer.kt index 5b36a07a..7fd7ca62 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/BottomDrawer.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/BottomDrawer.kt @@ -15,8 +15,10 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetDefaults +import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -33,13 +35,14 @@ import androidx.compose.ui.zIndex @Composable fun BottomDrawer( modifier: Modifier = Modifier, - drawerState: ModalBottomSheetState = androidx.compose.material.rememberModalBottomSheetState( - ModalBottomSheetValue.Hidden + drawerState: ModalBottomSheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + confirmStateChange = { it == ModalBottomSheetValue.Hidden || it == ModalBottomSheetValue.Expanded }, ), sheetContent: @Composable ColumnScope.() -> Unit = {}, content: @Composable () -> Unit = {}, ) { - androidx.compose.material.ModalBottomSheetLayout( + ModalBottomSheetLayout( modifier = modifier, sheetShape = RoundedCornerShape( topStart = 28.0.dp, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt index 3038f8a8..3a6a67de 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt @@ -46,9 +46,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import com.bobbyesp.spowlo.R -import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.common.intState import com.bobbyesp.spowlo.ui.components.AudioFilterChip import com.bobbyesp.spowlo.ui.components.BottomDrawer @@ -82,7 +80,6 @@ fun DownloaderSettingsDialog( dialogState: Boolean = false, isShareActivity: Boolean = false, drawerState: ModalBottomSheetState, - navController: NavController, confirm: () -> Unit, hide: () -> Unit, onRequestMetadata: () -> Unit, @@ -254,7 +251,7 @@ fun DownloaderSettingsDialog( label = stringResource(id = R.string.audio_quality), icon = Icons.Outlined.HighQuality, enabled = !preserveOriginalAudio, - onClick = { navController.navigate(Route.AUDIO_QUALITY_DIALOG) }, + onClick = { showAudioQualityDialog = true }, ) } DrawerSheetSubtitle(text = stringResource(id = R.string.spotify)) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index eb119fa1..a5073dd3 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -46,6 +46,7 @@ import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.MainActivity import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.features.mod_downloader.data.remote.ModsDownloaderAPI +import com.bobbyesp.spowlo.features.spotify_api.SpotifyApiRequests import com.bobbyesp.spowlo.ui.common.LocalWindowWidthState import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.common.animatedComposable @@ -81,7 +82,6 @@ import com.bobbyesp.spowlo.utils.UpdateUtil import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi -import com.google.accompanist.navigation.material.rememberBottomSheetNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -99,8 +99,7 @@ fun InitialEntry( modsDownloaderViewModel: ModsDownloaderViewModel, isUrlShared: Boolean ) { - val bottomSheetNavigator = rememberBottomSheetNavigator() - val navController = rememberAnimatedNavController(bottomSheetNavigator) + val navController = rememberAnimatedNavController() val navigationBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRootRoute = remember(navBackStackEntry) { @@ -229,7 +228,6 @@ fun InitialEntry( }, onNavigateToTaskList = { navController.navigate(Route.TASK_LIST) }, navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) }, - navController = navController, downloaderViewModel = downloaderViewModel ) } @@ -346,6 +344,14 @@ fun InitialEntry( } //} + LaunchedEffect(Unit){ + runCatching { + SpotifyApiRequests().buildApi() + }.onFailure { + it.printStackTrace() + ToastUtil.makeToastSuspend(context.getString(R.string.spotify_api_error)) + } + } LaunchedEffect(Unit) { if (!SPOTDL_UPDATE.getBoolean()) return@LaunchedEffect runCatching { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index fd7bb438..5db3b02e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -74,7 +74,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.Downloader import com.bobbyesp.spowlo.R @@ -107,7 +106,6 @@ fun DownloaderPage( onSongCardClicked: () -> Unit = {}, onNavigateToTaskList: () -> Unit = {}, navigateToMods: () -> Unit = {}, - navController: NavController, downloaderViewModel: DownloaderViewModel = hiltViewModel(), ) { val scope = rememberCoroutineScope() @@ -205,7 +203,6 @@ fun DownloaderPage( useDialog = useDialog, dialogState = showDownloadSettingDialog, drawerState = drawerState, - navController = navController, confirm = { checkPermissionOrDownload() }, onRequestMetadata = { downloaderViewModel.requestMetadata() }, hide = { downloaderViewModel.hideDialog(scope, useDialog) } @@ -459,7 +456,8 @@ fun InputUrl( ) { val progressAnimationValue by animateFloatAsState( targetValue = progress / 100f, - animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), + label = "" ) if (progressAnimationValue < 0) LinearProgressIndicator( modifier = Modifier diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00d83843..79f80822 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -258,4 +258,5 @@ SpotDL is up to date Geo bypass Use a localization bypass to download songs from countries that YT Music is restricted. + An error ocurred while trying to connect to the Spotify API \ No newline at end of file diff --git a/color/build.gradle.kts b/color/build.gradle.kts index c1bb3faa..51c77e3c 100644 --- a/color/build.gradle.kts +++ b/color/build.gradle.kts @@ -37,6 +37,6 @@ dependencies { api(libs.androidx.core.ktx) api(libs.androidx.compose.foundation) api(libs.androidx.compose.material3) - implementation("androidx.core:core-ktx:+") + implementation("androidx.core:core-ktx:1.10.0-rc01") } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3c5031eb..09678af1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,6 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +CLIENT_ID=abcad8ba647d4b0ebae797a8f444ac9b +CLIENT_SECRET=7ac6711e50044f1db20e4610f10f1f98 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 14796f37..013d3d7c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,9 +10,9 @@ androidxActivity = "1.6.1" markdownDependency = "0.3.2" navTransitions = "0.11.0-alpha" -androidxLifecycle = "2.6.0-rc01" +androidxLifecycle = "2.6.0" androidxNavigation = "2.5.3" -androidxComposeMaterial3 = "1.1.0-alpha07" +androidxComposeMaterial3 = "1.1.0-alpha08" androidxEspresso = "3.5.1" androidxHiltNavigationCompose = "1.0.0" From 1e82e48f5f5b14a1dd1aba680786beff25c40c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 11 Mar 2023 16:18:34 +0100 Subject: [PATCH 02/42] Added bottom navigation bar and also a new bottom sheet for settings and info --- .../java/com/bobbyesp/spowlo/MainActivity.kt | 22 -- .../spotify_api/SpotifyApiRequests.kt | 14 +- .../com/bobbyesp/spowlo/ui/common/Route.kt | 1 + .../bottomsheets/MoreOptionsBottomSheet.kt | 147 ++++++++ .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 334 ++++++++++-------- .../ui/pages/downloader/DownloaderPage.kt | 6 +- color/build.gradle.kts | 2 - gradle/libs.versions.toml | 4 +- 8 files changed, 353 insertions(+), 177 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/MoreOptionsBottomSheet.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt b/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt index f0b5f37a..4ff3a486 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt @@ -8,33 +8,12 @@ import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Download -import androidx.compose.material.icons.rounded.Home -import androidx.compose.material.icons.rounded.LibraryMusic -import androidx.compose.material.icons.rounded.MusicNote import androidx.compose.material.icons.rounded.Search -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat @@ -144,7 +123,6 @@ class MainActivity : AppCompatActivity() { val showInBottomNavigation = mapOf( Route.HOME to Icons.Rounded.Download, Route.SEARCHER to Icons.Rounded.Search, - Route.MEDIA_PLAYER to Icons.Rounded.MusicNote ) } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt index e5afe9af..0df78800 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt @@ -1,5 +1,6 @@ package com.bobbyesp.spowlo.features.spotify_api +import android.util.Log import com.adamratzman.spotify.SpotifyAppApi import com.adamratzman.spotify.models.SpotifySearchResult import com.adamratzman.spotify.spotifyAppApi @@ -13,17 +14,12 @@ class SpotifyApiRequests { private var api: SpotifyAppApi? = null - suspend fun buildApi(): SpotifyAppApi { - if (api == null) { - api = spotifyAppApi( - clientId = clientId, - clientSecret = clientSecret - ).build() - } - return api!! + suspend fun buildApi() { + Log.d("SpotifyApiRequests", "Building API with client ID: $clientId and client secret: $clientSecret") + api = spotifyAppApi(clientId, clientSecret).build() } suspend fun searchForTrack(query: String): SpotifySearchResult { - return api!!.search.search(query, limit = 50, offset = 1, market = Market.ES) + return api!!.search.searchAllTypes(query, limit = 50, offset = 1, market = Market.ES) } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt index 3f12f2b9..90c12a96 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt @@ -17,6 +17,7 @@ object Route { const val UPDATER_PAGE = "updater_page" const val MARKDOWN_VIEWER = "markdown_viewer" const val DOCUMENTATION = "documentation" + const val MORE_OPTIONS_HOME = "more_options_home" const val APPEARANCE = "appearance" const val APP_THEME = "app_theme" diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/MoreOptionsBottomSheet.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/MoreOptionsBottomSheet.kt new file mode 100644 index 00000000..09eb11e9 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/MoreOptionsBottomSheet.kt @@ -0,0 +1,147 @@ +package com.bobbyesp.spowlo.ui.dialogs.bottomsheets + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Code +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.bobbyesp.spowlo.App +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.common.Route + +@Composable +fun MoreOptionsHomeBottomSheet( + onBackPressed : () -> Unit, + navController: NavController +){ + val uriHandler = LocalUriHandler.current + + val roundedTopShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 0.dp, bottomEnd = 0.dp) + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .navigationBarsPadding() + .clip(roundedTopShape) + + ) { + BottomSheetHandle(modifier = Modifier.align(Alignment.CenterHorizontally)) + + Card( + modifier = Modifier.padding(horizontal = 16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) + ) { + ListItem( + leadingContent = { + Icon(imageVector = Icons.Rounded.Settings, contentDescription = null) + }, headlineContent = { + Text(text = stringResource(id = R.string.settings)) + }, modifier = Modifier.clickable(onClick = { + navController.navigate(Route.SETTINGS) + }), colors = ListItemDefaults.colors( + leadingIconColor = MaterialTheme.colorScheme.primary, + containerColor = Color.Transparent, + ) + ) + } + Column( + modifier = Modifier.padding(top = 16.dp).fillMaxWidth() + ){ + Text( + text = stringResource(id = R.string.app_name) + " " + App.packageInfo.versionName, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Text( + text = stringResource(id = R.string.app_description), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + + Row(Modifier.padding(horizontal = 4.dp)) { + IconButton(onClick = { + navController.navigate(Route.ABOUT) + }) { + Icon( + imageVector = Icons.Rounded.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + IconButton(onClick = { + uriHandler.openUri("https://github.com/BobbyESP/Spowlo") + }) { + Icon( + imageVector = Icons.Rounded.Code, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + +} + +@Composable +fun BottomSheetHandle( + modifier: Modifier = Modifier +) { + Divider( + modifier = modifier + .width(32.dp) + .padding(vertical = 14.dp) + .clip(CircleShape), + thickness = 4.dp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.4f) + ) +} + +@Composable +fun BottomSheetHeader( + modifier: Modifier = Modifier, + text: String +) { + Text( + text, + fontSize = 22.sp, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = modifier.fillMaxWidth() + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index a5073dd3..66bfa56c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -17,9 +17,17 @@ import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -32,6 +40,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination @@ -52,6 +61,7 @@ import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.common.animatedComposable import com.bobbyesp.spowlo.ui.common.slideInVerticallyComposable import com.bobbyesp.spowlo.ui.dialogs.UpdaterBottomDrawer +import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.MoreOptionsHomeBottomSheet import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderPage import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel import com.bobbyesp.spowlo.ui.pages.history.DownloadsHistoryPage @@ -82,8 +92,12 @@ import com.bobbyesp.spowlo.utils.UpdateUtil import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi +import com.google.accompanist.navigation.material.ModalBottomSheetLayout +import com.google.accompanist.navigation.material.bottomSheet +import com.google.accompanist.navigation.material.rememberBottomSheetNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -99,12 +113,26 @@ fun InitialEntry( modsDownloaderViewModel: ModsDownloaderViewModel, isUrlShared: Boolean ) { - val navController = rememberAnimatedNavController() + //bottom sheet remember state + val bottomSheetNavigator = rememberBottomSheetNavigator() + val navController = rememberAnimatedNavController(bottomSheetNavigator) val navigationBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRootRoute = remember(navBackStackEntry) { - navController.backQueue.getOrNull(1)?.destination?.route + mutableStateOf( + navController.currentBackStack.value.getOrNull(1)?.destination?.route, + ) } + + //every 1 second print the current root route + LaunchedEffect(currentRootRoute) { + while (true) { + delay(1000) + Log.d(TAG, "InitialEntry: ${currentRootRoute.value}") + } + } + + //navController.currentBackStack.value.getOrNull(1)?.destination?.route val shouldHideBottomNavBar = remember(navBackStackEntry) { navBackStackEntry?.destination?.hierarchy?.any { it.route == Route.SPOTIFY_SETUP } == true } @@ -161,30 +189,45 @@ fun InitialEntry( .fillMaxSize() .background(MaterialTheme.colorScheme.background) ) { - /*Scaffold( + ModalBottomSheetLayout( + bottomSheetNavigator, + sheetShape = MaterialTheme.shapes.extraLarge.copy(bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)), + scrimColor = MaterialTheme.colorScheme.scrim.copy(0.5f), + sheetBackgroundColor = MaterialTheme.colorScheme.surface) { + Scaffold( bottomBar = { NavigationBar( modifier = Modifier .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) .navigationBarsPadding(), ) { - MainActivity.showInBottomNavigation.forEach() { (route, icon) -> + MainActivity.showInBottomNavigation.forEach { (route, icon) -> val text = when (route) { Route.HOME -> App.context.getString(R.string.downloader) Route.SEARCHER -> App.context.getString(R.string.searcher) Route.MEDIA_PLAYER -> App.context.getString(R.string.mediaplayer) else -> "" } - NavigationBarItem(selected = currentRootRoute == route, - onClick = { - navController.navigate(route) { - popUpTo(navController.graph.startDestinationId) { - saveState = true + + val selected = currentRootRoute.value == route + + val onClick = remember(selected, navController, route) { + { + if (!selected) { + navController.navigate(route) { + popUpTo(navController.graph.id) { + saveState = true + } + + launchSingleTop = true + restoreState = true } - launchSingleTop = true - restoreState = true } - }, + } + } + NavigationBarItem( + selected = currentRootRoute.value == route, + onClick = onClick, icon = { Icon( imageVector = icon, @@ -201,148 +244,161 @@ fun InitialEntry( }) } } - }, modifier = Modifier.fillMaxSize().align(Alignment.Center)) { paddingValues ->*/ - - AnimatedNavHost( - modifier = Modifier - .fillMaxWidth( - when (LocalWindowWidthState.current) { - WindowWidthSizeClass.Compact -> 1f - WindowWidthSizeClass.Expanded -> 0.5f - else -> 0.8f + }, modifier = Modifier + .fillMaxSize() + .align(Alignment.Center)) { paddingValues -> + AnimatedNavHost( + modifier = Modifier + .fillMaxWidth( + when (LocalWindowWidthState.current) { + WindowWidthSizeClass.Compact -> 1f + WindowWidthSizeClass.Expanded -> 0.5f + else -> 0.8f + } + ) + .align(Alignment.Center) + .padding(bottom = paddingValues.calculateBottomPadding()), + navController = navController, + startDestination = Route.HOME + ) { + //TODO: Add all routes + animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME + DownloaderPage( + navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY) }, + navigateToSettings = { navController.navigate(Route.MORE_OPTIONS_HOME) }, + navigateToPlaylistPage = { navController.navigate(Route.PLAYLIST) }, + onSongCardClicked = { + navController.navigate(Route.PLAYLIST_METADATA_PAGE) + }, + onNavigateToTaskList = { navController.navigate(Route.TASK_LIST) }, + navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) }, + downloaderViewModel = downloaderViewModel + ) + } + animatedComposable(Route.SETTINGS) { + SettingsPage( + navController = navController + ) + } + animatedComposable(Route.GENERAL_DOWNLOAD_PREFERENCES) { + GeneralSettingsPage( + onBackPressed = onBackPressed + ) + } + animatedComposable(Route.DOWNLOADS_HISTORY) { + DownloadsHistoryPage( + onBackPressed = onBackPressed + ) + } + animatedComposable(Route.DOWNLOAD_DIRECTORY) { + DownloadsDirectoriesPage { + onBackPressed() } - ) - .align(Alignment.Center) - .padding(/*bottom = paddingValues.calculateBottomPadding()*/), - navController = navController, - startDestination = Route.HOME - ) { - //TODO: Add all routes - animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME - DownloaderPage( - navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY) }, - navigateToSettings = { navController.navigate(Route.SETTINGS) }, - navigateToPlaylistPage = { navController.navigate(Route.PLAYLIST) }, - onSongCardClicked = { - navController.navigate(Route.PLAYLIST_METADATA_PAGE) - }, - onNavigateToTaskList = { navController.navigate(Route.TASK_LIST) }, - navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) }, - downloaderViewModel = downloaderViewModel - ) - } - animatedComposable(Route.SETTINGS) { - SettingsPage( - navController = navController - ) - } - animatedComposable(Route.GENERAL_DOWNLOAD_PREFERENCES) { - GeneralSettingsPage( - onBackPressed = onBackPressed - ) - } - animatedComposable(Route.DOWNLOADS_HISTORY) { - DownloadsHistoryPage( - onBackPressed = onBackPressed - ) - } - animatedComposable(Route.DOWNLOAD_DIRECTORY) { - DownloadsDirectoriesPage { - onBackPressed() } - } - animatedComposable(Route.APPEARANCE) { - AppearancePage(navController = navController) - } - animatedComposable(Route.APP_THEME) { - AppThemePreferencesPage { - onBackPressed() + animatedComposable(Route.APPEARANCE) { + AppearancePage(navController = navController) } - } - animatedComposable(Route.DOWNLOAD_FORMAT) { - SettingsFormatsPage { - onBackPressed() + animatedComposable(Route.APP_THEME) { + AppThemePreferencesPage { + onBackPressed() + } } - } - animatedComposable(Route.SPOTIFY_PREFERENCES) { - SpotifySettingsPage { - onBackPressed() + animatedComposable(Route.DOWNLOAD_FORMAT) { + SettingsFormatsPage { + onBackPressed() + } + } + animatedComposable(Route.SPOTIFY_PREFERENCES) { + SpotifySettingsPage { + onBackPressed() + } + } + slideInVerticallyComposable(Route.PLAYLIST_METADATA_PAGE) { + PlaylistMetadataPage( + onBackPressed, + //TODO: ADD THE ABILITY TO PASS JUST SONGS AND NOT GET THEM FROM THE MUTABLE STATE + ) + } + animatedComposable(Route.MODS_DOWNLOADER) { + ModsDownloaderPage( + onBackPressed, + modsDownloaderViewModel + ) + } + animatedComposable(Route.COOKIE_PROFILE) { + CookieProfilePage( + cookiesViewModel = cookiesViewModel, + navigateToCookieGeneratorPage = { navController.navigate(Route.COOKIE_GENERATOR_WEBVIEW) }, + ) { onBackPressed() } + } + animatedComposable( + Route.COOKIE_GENERATOR_WEBVIEW + ) { + WebViewPage(cookiesViewModel) { onBackPressed() } + } + animatedComposable(Route.UPDATER_PAGE) { + UpdaterPage( + onBackPressed + ) + } + animatedComposable(Route.DOCUMENTATION) { + DocumentationPage( + onBackPressed, + navController + ) } - } - slideInVerticallyComposable(Route.PLAYLIST_METADATA_PAGE) { - PlaylistMetadataPage( - onBackPressed, - //TODO: ADD THE ABILITY TO PASS JUST SONGS AND NOT GET THEM FROM THE MUTABLE STATE - ) - } - animatedComposable(Route.MODS_DOWNLOADER) { - ModsDownloaderPage( - onBackPressed, - modsDownloaderViewModel - ) - } - animatedComposable(Route.COOKIE_PROFILE) { - CookieProfilePage( - cookiesViewModel = cookiesViewModel, - navigateToCookieGeneratorPage = { navController.navigate(Route.COOKIE_GENERATOR_WEBVIEW) }, - ) { onBackPressed() } - } - animatedComposable( - Route.COOKIE_GENERATOR_WEBVIEW - ) { - WebViewPage(cookiesViewModel) { onBackPressed() } - } - animatedComposable(Route.UPDATER_PAGE) { - UpdaterPage( - onBackPressed - ) - } - animatedComposable(Route.DOCUMENTATION) { - DocumentationPage( - onBackPressed, - navController - ) - } - animatedComposable(Route.ABOUT) { - AboutPage { - onBackPressed() + animatedComposable(Route.ABOUT) { + AboutPage { + onBackPressed() + } } - } - animatedComposable(Route.LANGUAGES) { - LanguagePage { - onBackPressed() + animatedComposable(Route.LANGUAGES) { + LanguagePage { + onBackPressed() + } } - } - navDeepLink { - // Want to go to "markdown_viewer/{markdownFileName}" - uriPattern = "android-app://androidx.navigation/markdown_viewer/{markdownFileName}" - } + navDeepLink { + // Want to go to "markdown_viewer/{markdownFileName}" + uriPattern = + "android-app://androidx.navigation/markdown_viewer/{markdownFileName}" + } - animatedComposable( - "markdown_viewer/{markdownFileName}", - arguments = listOf(navArgument("markdownFileName") { type = NavType.StringType }) - ) { backStackEntry -> - val mdFileName = backStackEntry.arguments?.getString("markdownFileName") ?: "" - Log.d("MainActivity", mdFileName) - MarkdownViewerPage( - markdownFileName = mdFileName, - onBackPressed = onBackPressed - ) - } + animatedComposable( + "markdown_viewer/{markdownFileName}", + arguments = listOf(navArgument("markdownFileName") { + type = NavType.StringType + }) + ) { backStackEntry -> + val mdFileName = backStackEntry.arguments?.getString("markdownFileName") ?: "" + Log.d("MainActivity", mdFileName) + MarkdownViewerPage( + markdownFileName = mdFileName, + onBackPressed = onBackPressed + ) + } + + //DIALOGS + //TODO: ADD DIALOGS + dialog(Route.AUDIO_QUALITY_DIALOG) { + AudioQualityDialog( + onBackPressed + ) + } - //DIALOGS - //TODO: ADD DIALOGS - dialog(Route.AUDIO_QUALITY_DIALOG) { - AudioQualityDialog( - onBackPressed - ) + //BOTTOM SHEETS + bottomSheet(Route.MORE_OPTIONS_HOME) { + MoreOptionsHomeBottomSheet( + onBackPressed, + navController + ) + } } } } -//} +} LaunchedEffect(Unit){ runCatching { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index 5db3b02e..90a914a6 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -33,7 +33,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ContentPaste import androidx.compose.material.icons.outlined.Error import androidx.compose.material.icons.outlined.FileDownload -import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.List import androidx.compose.material.icons.outlined.Subscriptions import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -240,8 +240,8 @@ fun DownloaderPageImplementation( navigationIcon = { IconButton(onClick = { navigateToSettings() }) { Icon( - imageVector = Icons.Outlined.Settings, - contentDescription = stringResource(id = R.string.settings) + imageVector = Icons.Outlined.List, + contentDescription = stringResource(id = R.string.show_more_actions) ) } }, actions = { diff --git a/color/build.gradle.kts b/color/build.gradle.kts index 51c77e3c..89829254 100644 --- a/color/build.gradle.kts +++ b/color/build.gradle.kts @@ -37,6 +37,4 @@ dependencies { api(libs.androidx.core.ktx) api(libs.androidx.compose.foundation) api(libs.androidx.compose.material3) - implementation("androidx.core:core-ktx:1.10.0-rc01") - } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 013d3d7c..debf1d93 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -accompanist = "0.28.0" +accompanist = "0.29.2-rc" androidGradlePlugin = "7.4.0" androidxComposeBom = "2023.01.00" androidxComposeCompiler = "1.4.0" -androidxCore = "1.9.0" +androidxCore = "1.10.0-rc01" androidMaterial = "1.9.0-alpha02" androidxAppCompat = "1.7.0-alpha02" androidxActivity = "1.6.1" From dfdbe9eb02106e6b073b419210a6fc8e799a2b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 11 Mar 2023 18:41:17 +0100 Subject: [PATCH 03/42] tests: added spotify api tests --- app/build.gradle.kts | 24 ++++++++++++++-- .../spotify_api/SpotifyApiRequests.kt | 28 ++++++++++++++++--- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 25 +++++++++-------- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index db53f9dd..8930de3f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -110,24 +110,42 @@ android { proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + packagingOptions { + resources.excludes.add("META-INF/*.kotlin_module") + } if (keystorePropertiesFile.exists()) signingConfig = signingConfigs.getByName("debug") - //add client id and secret to build config buildConfigField("String", "CLIENT_ID", "\"${project.properties["CLIENT_ID"]}\"") - buildConfigField("String", "CLIENT_SECRET", "\"${project.properties["CLIENT_SECRET"]}\"") + buildConfigField( + "String", + "CLIENT_SECRET", + "\"${project.properties["CLIENT_SECRET"]}\"" + ) + matchingFallbacks.add(0, "debug") + matchingFallbacks.add(1, "release") } debug { if (keystorePropertiesFile.exists()) signingConfig = signingConfigs.getByName("debug") + packagingOptions { + resources.excludes.add("META-INF/*.kotlin_module") + } buildConfigField("String", "CLIENT_ID", "\"${project.properties["CLIENT_ID"]}\"") - buildConfigField("String", "CLIENT_SECRET", "\"${project.properties["CLIENT_SECRET"]}\"") + buildConfigField( + "String", + "CLIENT_SECRET", + "\"${project.properties["CLIENT_SECRET"]}\"" + ) + matchingFallbacks.add(0, "debug") + matchingFallbacks.add(1, "release") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } + kotlinOptions { jvmTarget = "1.8" } diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt index 0df78800..ddb91324 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt @@ -2,24 +2,44 @@ package com.bobbyesp.spowlo.features.spotify_api import android.util.Log import com.adamratzman.spotify.SpotifyAppApi +import com.adamratzman.spotify.models.SpotifyPublicUser import com.adamratzman.spotify.models.SpotifySearchResult import com.adamratzman.spotify.spotifyAppApi import com.adamratzman.spotify.utils.Market import com.bobbyesp.spowlo.BuildConfig +import kotlinx.coroutines.Job -class SpotifyApiRequests { +object SpotifyApiRequests { private val clientId = BuildConfig.CLIENT_ID private val clientSecret = BuildConfig.CLIENT_SECRET - private var api: SpotifyAppApi? = null + private var currentJob: Job? = null + + + //Pulls the clientId and clientSecret tokens and builds them into an object + suspend fun buildApi() { Log.d("SpotifyApiRequests", "Building API with client ID: $clientId and client secret: $clientSecret") api = spotifyAppApi(clientId, clientSecret).build() } - suspend fun searchForTrack(query: String): SpotifySearchResult { - return api!!.search.searchAllTypes(query, limit = 50, offset = 1, market = Market.ES) + //Performs Spotify database query for queries related to user information. + suspend fun userSearch(userQuery: String): SpotifyPublicUser? { + return api!!.users.getProfile(userQuery) + } + + // Performs Spotify database query for queries related to track information. + suspend fun trackSearch(searchQuery: String): SpotifySearchResult { + kotlin.runCatching { + api!!.search.searchAllTypes(searchQuery, limit = 50, offset = 1, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return SpotifySearchResult() + }.onSuccess { + return it + } + return SpotifySearchResult() } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 66bfa56c..4f55b43a 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -52,6 +52,7 @@ import androidx.navigation.navArgument import androidx.navigation.navDeepLink import com.bobbyesp.library.SpotDL import com.bobbyesp.spowlo.App +import com.bobbyesp.spowlo.BuildConfig import com.bobbyesp.spowlo.MainActivity import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.features.mod_downloader.data.remote.ModsDownloaderAPI @@ -97,7 +98,6 @@ import com.google.accompanist.navigation.material.bottomSheet import com.google.accompanist.navigation.material.rememberBottomSheetNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -124,14 +124,6 @@ fun InitialEntry( ) } - //every 1 second print the current root route - LaunchedEffect(currentRootRoute) { - while (true) { - delay(1000) - Log.d(TAG, "InitialEntry: ${currentRootRoute.value}") - } - } - //navController.currentBackStack.value.getOrNull(1)?.destination?.route val shouldHideBottomNavBar = remember(navBackStackEntry) { navBackStackEntry?.destination?.hierarchy?.any { it.route == Route.SPOTIFY_SETUP } == true @@ -400,14 +392,25 @@ fun InitialEntry( } } - LaunchedEffect(Unit){ + if(BuildConfig.DEBUG) LaunchedEffect(Unit){ runCatching { - SpotifyApiRequests().buildApi() + SpotifyApiRequests.buildApi() }.onFailure { it.printStackTrace() ToastUtil.makeToastSuspend(context.getString(R.string.spotify_api_error)) + }.onSuccess { + val req = SpotifyApiRequests.trackSearch("Faded Alan Walker") + Log.d("InitialEntry", "Name:" + req.tracks!![0].name) + Log.d("InitialEntry", "Artist:" + req.tracks!![0].artists[0].name) + Log.d("InitialEntry", "Album:" + req.tracks!![0].album.name) + Log.d("InitialEntry", "Album Image:" + req.tracks!![0].album.images[0].url) + Log.d("InitialEntry", "Duration:" + req.tracks!![0].durationMs) + Log.d("InitialEntry", "Popularity:" + req.tracks!![0].popularity) + Log.d("InitialEntry", "-------------------------------------------") + Log.d("InitialEntry", "Full response: $req") } } + LaunchedEffect(Unit) { if (!SPOTDL_UPDATE.getBoolean()) return@LaunchedEffect runCatching { From 8bed4718965c7c33bdb13904c17f7b5b32359a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 11 Mar 2023 21:05:42 +0100 Subject: [PATCH 04/42] beauty: Added corners to the bottom sheets --- .../com/bobbyesp/spowlo/ui/common/Route.kt | 1 + .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 442 +++++++++--------- .../com/bobbyesp/spowlo/ui/theme/Theme.kt | 7 - 3 files changed, 228 insertions(+), 222 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt index 90c12a96..ec193ee1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt @@ -18,6 +18,7 @@ object Route { const val MARKDOWN_VIEWER = "markdown_viewer" const val DOCUMENTATION = "documentation" const val MORE_OPTIONS_HOME = "more_options_home" + const val SONG_INFO_HISTORY = "song_info_history" const val APPEARANCE = "appearance" const val APP_THEME = "app_theme" diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 4f55b43a..b75b5045 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -8,15 +8,17 @@ import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize @@ -116,8 +118,8 @@ fun InitialEntry( //bottom sheet remember state val bottomSheetNavigator = rememberBottomSheetNavigator() val navController = rememberAnimatedNavController(bottomSheetNavigator) - val navigationBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRootRoute = remember(navBackStackEntry) { mutableStateOf( navController.currentBackStack.value.getOrNull(1)?.destination?.route, @@ -126,7 +128,7 @@ fun InitialEntry( //navController.currentBackStack.value.getOrNull(1)?.destination?.route val shouldHideBottomNavBar = remember(navBackStackEntry) { - navBackStackEntry?.destination?.hierarchy?.any { it.route == Route.SPOTIFY_SETUP } == true + navBackStackEntry?.destination?.hierarchy?.any { it.route == Route.SPOTIFY_SETUP || it.route == Route.DOWNLOADS_HISTORY } == true } val isLandscape = remember { MutableTransitionState(false) } @@ -183,216 +185,226 @@ fun InitialEntry( ) { ModalBottomSheetLayout( bottomSheetNavigator, - sheetShape = MaterialTheme.shapes.extraLarge.copy(bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)), + sheetShape = MaterialTheme.shapes.medium.copy( + bottomStart = CornerSize(0.dp), + bottomEnd = CornerSize(0.dp) + ), scrimColor = MaterialTheme.colorScheme.scrim.copy(0.5f), - sheetBackgroundColor = MaterialTheme.colorScheme.surface) { - Scaffold( - bottomBar = { - NavigationBar( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) - .navigationBarsPadding(), + sheetBackgroundColor = MaterialTheme.colorScheme.surface, + ) { + Scaffold( + bottomBar = { + AnimatedVisibility(visible = !shouldHideBottomNavBar, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() ) { - MainActivity.showInBottomNavigation.forEach { (route, icon) -> - val text = when (route) { - Route.HOME -> App.context.getString(R.string.downloader) - Route.SEARCHER -> App.context.getString(R.string.searcher) - Route.MEDIA_PLAYER -> App.context.getString(R.string.mediaplayer) - else -> "" - } + NavigationBar( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) + .navigationBarsPadding(), + ) { + MainActivity.showInBottomNavigation.forEach { (route, icon) -> + val text = when (route) { + Route.HOME -> App.context.getString(R.string.downloader) + Route.SEARCHER -> App.context.getString(R.string.searcher) + else -> "" + } - val selected = currentRootRoute.value == route + val selected = currentRootRoute.value == route - val onClick = remember(selected, navController, route) { - { - if (!selected) { - navController.navigate(route) { - popUpTo(navController.graph.id) { - saveState = true - } + val onClick = remember(selected, navController, route) { + { + if (!selected) { + navController.navigate(route) { + popUpTo(navController.graph.id) { + saveState = true + } - launchSingleTop = true - restoreState = true + launchSingleTop = true + restoreState = true + } } } } + NavigationBarItem( + selected = currentRootRoute.value == route, + onClick = onClick, + icon = { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + }, + label = { + Text( + text = text, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface + ) + }) } - NavigationBarItem( - selected = currentRootRoute.value == route, - onClick = onClick, - icon = { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) - }, - label = { - Text( - text = text, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }) } } }, modifier = Modifier - .fillMaxSize() - .align(Alignment.Center)) { paddingValues -> - AnimatedNavHost( - modifier = Modifier - .fillMaxWidth( - when (LocalWindowWidthState.current) { - WindowWidthSizeClass.Compact -> 1f - WindowWidthSizeClass.Expanded -> 0.5f - else -> 0.8f - } - ) + .fillMaxSize() .align(Alignment.Center) - .padding(bottom = paddingValues.calculateBottomPadding()), - navController = navController, - startDestination = Route.HOME - ) { - //TODO: Add all routes - animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME - DownloaderPage( - navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY) }, - navigateToSettings = { navController.navigate(Route.MORE_OPTIONS_HOME) }, - navigateToPlaylistPage = { navController.navigate(Route.PLAYLIST) }, - onSongCardClicked = { - navController.navigate(Route.PLAYLIST_METADATA_PAGE) - }, - onNavigateToTaskList = { navController.navigate(Route.TASK_LIST) }, - navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) }, - downloaderViewModel = downloaderViewModel - ) - } - animatedComposable(Route.SETTINGS) { - SettingsPage( - navController = navController - ) - } - animatedComposable(Route.GENERAL_DOWNLOAD_PREFERENCES) { - GeneralSettingsPage( - onBackPressed = onBackPressed - ) - } - animatedComposable(Route.DOWNLOADS_HISTORY) { - DownloadsHistoryPage( - onBackPressed = onBackPressed - ) - } - animatedComposable(Route.DOWNLOAD_DIRECTORY) { - DownloadsDirectoriesPage { - onBackPressed() + ) { paddingValues -> + AnimatedNavHost( + modifier = Modifier + .fillMaxWidth( + when (LocalWindowWidthState.current) { + WindowWidthSizeClass.Compact -> 1f + WindowWidthSizeClass.Expanded -> 0.5f + else -> 0.8f + } + ) + .align(Alignment.Center) + .padding(bottom = paddingValues.calculateBottomPadding()), + navController = navController, + startDestination = Route.HOME + ) { + //TODO: Add all routes + animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME + DownloaderPage( + navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY) }, + navigateToSettings = { navController.navigate(Route.MORE_OPTIONS_HOME) }, + navigateToPlaylistPage = { navController.navigate(Route.PLAYLIST) }, + onSongCardClicked = { + navController.navigate(Route.PLAYLIST_METADATA_PAGE) + }, + onNavigateToTaskList = { navController.navigate(Route.TASK_LIST) }, + navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) }, + downloaderViewModel = downloaderViewModel + ) } - } - animatedComposable(Route.APPEARANCE) { - AppearancePage(navController = navController) - } - animatedComposable(Route.APP_THEME) { - AppThemePreferencesPage { - onBackPressed() + animatedComposable(Route.SETTINGS) { + SettingsPage( + navController = navController + ) } - } - animatedComposable(Route.DOWNLOAD_FORMAT) { - SettingsFormatsPage { - onBackPressed() + animatedComposable(Route.GENERAL_DOWNLOAD_PREFERENCES) { + GeneralSettingsPage( + onBackPressed = onBackPressed + ) } - } - animatedComposable(Route.SPOTIFY_PREFERENCES) { - SpotifySettingsPage { - onBackPressed() + animatedComposable(Route.DOWNLOADS_HISTORY) { + DownloadsHistoryPage( + onBackPressed = onBackPressed, + ) + } + animatedComposable(Route.DOWNLOAD_DIRECTORY) { + DownloadsDirectoriesPage { + onBackPressed() + } + } + animatedComposable(Route.APPEARANCE) { + AppearancePage(navController = navController) + } + animatedComposable(Route.APP_THEME) { + AppThemePreferencesPage { + onBackPressed() + } + } + animatedComposable(Route.DOWNLOAD_FORMAT) { + SettingsFormatsPage { + onBackPressed() + } + } + animatedComposable(Route.SPOTIFY_PREFERENCES) { + SpotifySettingsPage { + onBackPressed() + } + } + slideInVerticallyComposable(Route.PLAYLIST_METADATA_PAGE) { + PlaylistMetadataPage( + onBackPressed, + //TODO: ADD THE ABILITY TO PASS JUST SONGS AND NOT GET THEM FROM THE MUTABLE STATE + ) + } + animatedComposable(Route.MODS_DOWNLOADER) { + ModsDownloaderPage( + onBackPressed, + modsDownloaderViewModel + ) + } + animatedComposable(Route.COOKIE_PROFILE) { + CookieProfilePage( + cookiesViewModel = cookiesViewModel, + navigateToCookieGeneratorPage = { navController.navigate(Route.COOKIE_GENERATOR_WEBVIEW) }, + ) { onBackPressed() } + } + animatedComposable( + Route.COOKIE_GENERATOR_WEBVIEW + ) { + WebViewPage(cookiesViewModel) { onBackPressed() } + } + animatedComposable(Route.UPDATER_PAGE) { + UpdaterPage( + onBackPressed + ) + } + animatedComposable(Route.DOCUMENTATION) { + DocumentationPage( + onBackPressed, + navController + ) } - } - slideInVerticallyComposable(Route.PLAYLIST_METADATA_PAGE) { - PlaylistMetadataPage( - onBackPressed, - //TODO: ADD THE ABILITY TO PASS JUST SONGS AND NOT GET THEM FROM THE MUTABLE STATE - ) - } - animatedComposable(Route.MODS_DOWNLOADER) { - ModsDownloaderPage( - onBackPressed, - modsDownloaderViewModel - ) - } - animatedComposable(Route.COOKIE_PROFILE) { - CookieProfilePage( - cookiesViewModel = cookiesViewModel, - navigateToCookieGeneratorPage = { navController.navigate(Route.COOKIE_GENERATOR_WEBVIEW) }, - ) { onBackPressed() } - } - animatedComposable( - Route.COOKIE_GENERATOR_WEBVIEW - ) { - WebViewPage(cookiesViewModel) { onBackPressed() } - } - animatedComposable(Route.UPDATER_PAGE) { - UpdaterPage( - onBackPressed - ) - } - animatedComposable(Route.DOCUMENTATION) { - DocumentationPage( - onBackPressed, - navController - ) - } - animatedComposable(Route.ABOUT) { - AboutPage { - onBackPressed() + animatedComposable(Route.ABOUT) { + AboutPage { + onBackPressed() + } } - } - animatedComposable(Route.LANGUAGES) { - LanguagePage { - onBackPressed() + animatedComposable(Route.LANGUAGES) { + LanguagePage { + onBackPressed() + } } - } - navDeepLink { - // Want to go to "markdown_viewer/{markdownFileName}" - uriPattern = - "android-app://androidx.navigation/markdown_viewer/{markdownFileName}" - } + navDeepLink { + // Want to go to "markdown_viewer/{markdownFileName}" + uriPattern = + "android-app://androidx.navigation/markdown_viewer/{markdownFileName}" + } - animatedComposable( - "markdown_viewer/{markdownFileName}", - arguments = listOf(navArgument("markdownFileName") { - type = NavType.StringType - }) - ) { backStackEntry -> - val mdFileName = backStackEntry.arguments?.getString("markdownFileName") ?: "" - Log.d("MainActivity", mdFileName) - MarkdownViewerPage( - markdownFileName = mdFileName, - onBackPressed = onBackPressed - ) - } + animatedComposable( + "markdown_viewer/{markdownFileName}", + arguments = listOf(navArgument("markdownFileName") { + type = NavType.StringType + }) + ) { backStackEntry -> + val mdFileName = + backStackEntry.arguments?.getString("markdownFileName") ?: "" + Log.d("MainActivity", mdFileName) + MarkdownViewerPage( + markdownFileName = mdFileName, + onBackPressed = onBackPressed + ) + } - //DIALOGS - //TODO: ADD DIALOGS - dialog(Route.AUDIO_QUALITY_DIALOG) { - AudioQualityDialog( - onBackPressed - ) - } + //DIALOGS ------------------------------- + //TODO: ADD DIALOGS + dialog(Route.AUDIO_QUALITY_DIALOG) { + AudioQualityDialog( + onBackPressed + ) + } - //BOTTOM SHEETS - bottomSheet(Route.MORE_OPTIONS_HOME) { - MoreOptionsHomeBottomSheet( - onBackPressed, - navController - ) + //BOTTOM SHEETS -------------------------- + bottomSheet(Route.MORE_OPTIONS_HOME) { + MoreOptionsHomeBottomSheet( + onBackPressed, + navController + ) + } } } } } -} - if(BuildConfig.DEBUG) LaunchedEffect(Unit){ + if (BuildConfig.DEBUG) LaunchedEffect(Unit) { runCatching { SpotifyApiRequests.buildApi() }.onFailure { @@ -432,7 +444,7 @@ fun InitialEntry( latestRelease = it showUpdateDialog = true } - if(showUpdateDialog){ + if (showUpdateDialog) { UpdateUtil.showUpdateDrawer() } }.onFailure { @@ -451,35 +463,35 @@ fun InitialEntry( } if (showUpdateDialog) { - /*UpdateDialogImpl( - onDismissRequest = { - showUpdateDialog = false - updateJob?.cancel() - }, - title = latestRelease.name.toString(), - onConfirmUpdate = { - updateJob = scope.launch(Dispatchers.IO) { - runCatching { - UpdateUtil.downloadApk(latestRelease = latestRelease) - .collect { downloadStatus -> - currentDownloadStatus = downloadStatus - if (downloadStatus is UpdateUtil.DownloadStatus.Finished) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - launcher.launch(Manifest.permission.REQUEST_INSTALL_PACKAGES) - } - } - } - }.onFailure { - it.printStackTrace() - currentDownloadStatus = UpdateUtil.DownloadStatus.NotYet - ToastUtil.makeToastSuspend(context.getString(R.string.app_update_failed)) - return@launch - } - } - }, - releaseNote = latestRelease.body.toString(), - downloadStatus = currentDownloadStatus - )*/ + /*UpdateDialogImpl( + onDismissRequest = { + showUpdateDialog = false + updateJob?.cancel() + }, + title = latestRelease.name.toString(), + onConfirmUpdate = { + updateJob = scope.launch(Dispatchers.IO) { + runCatching { + UpdateUtil.downloadApk(latestRelease = latestRelease) + .collect { downloadStatus -> + currentDownloadStatus = downloadStatus + if (downloadStatus is UpdateUtil.DownloadStatus.Finished) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + launcher.launch(Manifest.permission.REQUEST_INSTALL_PACKAGES) + } + } + } + }.onFailure { + it.printStackTrace() + currentDownloadStatus = UpdateUtil.DownloadStatus.NotYet + ToastUtil.makeToastSuspend(context.getString(R.string.app_update_failed)) + return@launch + } + } + }, + releaseNote = latestRelease.body.toString(), + downloadStatus = currentDownloadStatus + )*/ UpdaterBottomDrawer(latestRelease = latestRelease) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/theme/Theme.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/theme/Theme.kt index 1b18406e..77e6eaa1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/theme/Theme.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/theme/Theme.kt @@ -3,25 +3,19 @@ package com.bobbyesp.spowlo.ui.theme import android.app.Activity import android.content.Context import android.content.ContextWrapper -import android.os.Build import android.view.Window import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle -import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.style.LineBreak -import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.android.material.color.DynamicColors @@ -48,7 +42,6 @@ private tailrec fun Context.findWindow(): Window? = else -> null } -@OptIn(ExperimentalTextApi::class) @Composable fun SpowloTheme( darkTheme: Boolean = isSystemInDarkTheme(), From 89103dd1021145247be89b38917b080db1e5380b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sun, 12 Mar 2023 00:33:56 +0100 Subject: [PATCH 05/42] feat: Added Spotify search with UI impl --- .../spotify_api/SpotifyApiRequests.kt | 19 ++- .../com/bobbyesp/spowlo/ui/common/Route.kt | 2 + .../search_feat/SearchingSongComponent.kt | 93 +++++++++++ .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 19 ++- .../ui/pages/downloader/DownloaderPage.kt | 4 +- .../spowlo/ui/pages/searcher/SearcherPage.kt | 145 ++++++++++++++++++ .../pages/searcher/SearcherPageViewModel.kt | 53 +++++++ app/src/main/res/values/strings.xml | 2 + 8 files changed, 329 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt index ddb91324..10445b7a 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt @@ -4,6 +4,7 @@ import android.util.Log import com.adamratzman.spotify.SpotifyAppApi import com.adamratzman.spotify.models.SpotifyPublicUser import com.adamratzman.spotify.models.SpotifySearchResult +import com.adamratzman.spotify.models.Token import com.adamratzman.spotify.spotifyAppApi import com.adamratzman.spotify.utils.Market import com.bobbyesp.spowlo.BuildConfig @@ -14,6 +15,7 @@ object SpotifyApiRequests { private val clientId = BuildConfig.CLIENT_ID private val clientSecret = BuildConfig.CLIENT_SECRET private var api: SpotifyAppApi? = null + private var token: Token? = null private var currentJob: Job? = null @@ -22,7 +24,10 @@ object SpotifyApiRequests { suspend fun buildApi() { Log.d("SpotifyApiRequests", "Building API with client ID: $clientId and client secret: $clientSecret") - api = spotifyAppApi(clientId, clientSecret).build() + token = spotifyAppApi(clientId, clientSecret).build().token + api = spotifyAppApi(clientId, clientSecret, token!!) { + automaticRefresh = true + }.build() } //Performs Spotify database query for queries related to user information. @@ -42,4 +47,16 @@ object SpotifyApiRequests { } return SpotifySearchResult() } + + suspend fun searchAllTypes(searchQuery: String): SpotifySearchResult { + kotlin.runCatching { + api!!.search.searchAllTypes(searchQuery, limit = 50, offset = 1, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return SpotifySearchResult() + }.onSuccess { + return it + } + return SpotifySearchResult() + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt index ec193ee1..70ee8094 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt @@ -1,6 +1,8 @@ package com.bobbyesp.spowlo.ui.common object Route { + + const val NavGraph = "nav_graph" const val HOME = "home" const val DOWNLOADER = "downloader" const val DOWNLOADS_HISTORY = "download_history" diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt new file mode 100644 index 00000000..3e3e7301 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt @@ -0,0 +1,93 @@ +package com.bobbyesp.spowlo.ui.components.songs.search_feat + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.bobbyesp.spowlo.ui.common.AsyncImageImpl +import com.bobbyesp.spowlo.ui.components.MarqueeText +import com.bobbyesp.spowlo.utils.ChromeCustomTabsUtil + +@Composable +fun SearchingSongComponent( + artworkUrl: String, + songName: String, + artists: String, + spotifyUrl: String +) { + Column(Modifier.fillMaxWidth().clickable { ChromeCustomTabsUtil.openUrl(spotifyUrl) }.padding(8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, //This makes all go to the center + ) { + AsyncImageImpl( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 6.dp) + .size(72.dp) + .aspectRatio(1f, matchHeightConstraintsFirst = true) + .clip(MaterialTheme.shapes.small), + model = artworkUrl, + contentDescription = "Song cover", + contentScale = ContentScale.Crop, + isPreview = false + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Column( + modifier = Modifier + .padding(6.dp) + .weight(1f), //Weight is to make the time not go away from the screen + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + MarqueeText( + text = songName, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + basicGradientColor = MaterialTheme.colorScheme.surface.copy( + alpha = 0.8f + ), + ) + } + Spacer(Modifier.height(8.dp)) + MarqueeText( + text = artists, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 12.sp, + basicGradientColor = MaterialTheme.colorScheme.surface.copy( + alpha = 0.8f + ), + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index b75b5045..f89f5401 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -62,6 +62,7 @@ import com.bobbyesp.spowlo.features.spotify_api.SpotifyApiRequests import com.bobbyesp.spowlo.ui.common.LocalWindowWidthState import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.common.animatedComposable +import com.bobbyesp.spowlo.ui.common.animatedComposableVariant import com.bobbyesp.spowlo.ui.common.slideInVerticallyComposable import com.bobbyesp.spowlo.ui.dialogs.UpdaterBottomDrawer import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.MoreOptionsHomeBottomSheet @@ -71,6 +72,7 @@ import com.bobbyesp.spowlo.ui.pages.history.DownloadsHistoryPage import com.bobbyesp.spowlo.ui.pages.mod_downloader.ModsDownloaderPage import com.bobbyesp.spowlo.ui.pages.mod_downloader.ModsDownloaderViewModel import com.bobbyesp.spowlo.ui.pages.playlist.PlaylistMetadataPage +import com.bobbyesp.spowlo.ui.pages.searcher.SearcherPage import com.bobbyesp.spowlo.ui.pages.settings.SettingsPage import com.bobbyesp.spowlo.ui.pages.settings.about.AboutPage import com.bobbyesp.spowlo.ui.pages.settings.appearance.AppThemePreferencesPage @@ -128,7 +130,7 @@ fun InitialEntry( //navController.currentBackStack.value.getOrNull(1)?.destination?.route val shouldHideBottomNavBar = remember(navBackStackEntry) { - navBackStackEntry?.destination?.hierarchy?.any { it.route == Route.SPOTIFY_SETUP || it.route == Route.DOWNLOADS_HISTORY } == true + navBackStackEntry?.destination?.hierarchy?.any { it.route == Route.SPOTIFY_SETUP } == true } val isLandscape = remember { MutableTransitionState(false) } @@ -216,10 +218,9 @@ fun InitialEntry( { if (!selected) { navController.navigate(route) { - popUpTo(navController.graph.id) { + popUpTo(Route.NavGraph) { saveState = true } - launchSingleTop = true restoreState = true } @@ -255,14 +256,15 @@ fun InitialEntry( .fillMaxWidth( when (LocalWindowWidthState.current) { WindowWidthSizeClass.Compact -> 1f - WindowWidthSizeClass.Expanded -> 0.5f + WindowWidthSizeClass.Expanded -> 1f else -> 0.8f } ) .align(Alignment.Center) .padding(bottom = paddingValues.calculateBottomPadding()), navController = navController, - startDestination = Route.HOME + startDestination = Route.HOME, + route = Route.NavGraph ) { //TODO: Add all routes animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME @@ -363,6 +365,11 @@ fun InitialEntry( } } + animatedComposableVariant(Route.SEARCHER) { + SearcherPage( + ) + } + navDeepLink { // Want to go to "markdown_viewer/{markdownFileName}" uriPattern = @@ -399,6 +406,8 @@ fun InitialEntry( navController ) } + + //Can add the downloads history bottom sheet here using `val downloadsHistoryViewModel = hiltViewModel()` } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index 90a914a6..96d26929 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -33,7 +33,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ContentPaste import androidx.compose.material.icons.outlined.Error import androidx.compose.material.icons.outlined.FileDownload -import androidx.compose.material.icons.outlined.List +import androidx.compose.material.icons.outlined.FormatListBulleted import androidx.compose.material.icons.outlined.Subscriptions import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -240,7 +240,7 @@ fun DownloaderPageImplementation( navigationIcon = { IconButton(onClick = { navigateToSettings() }) { Icon( - imageVector = Icons.Outlined.List, + imageVector = Icons.Outlined.FormatListBulleted, contentDescription = stringResource(id = R.string.show_more_actions) ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt new file mode 100644 index 00000000..5ee87383 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -0,0 +1,145 @@ +package com.bobbyesp.spowlo.ui.pages.searcher + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.components.songs.search_feat.SearchingSongComponent +import kotlinx.coroutines.delay + +@Composable +fun SearcherPage( + searcherPageViewModel: SearcherPageViewModel = hiltViewModel() +) { + val viewState by searcherPageViewModel.viewStateFlow.collectAsStateWithLifecycle() + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) { + SearcherPageImpl( + viewState = viewState, + onValueChange = { query -> + searcherPageViewModel.updateSearchText(query) + } + ) + } + LaunchedEffect(viewState.query) { + if (viewState.query.isEmpty()) return@LaunchedEffect + delay(300) + searcherPageViewModel.makeSearch() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearcherPageImpl( + viewState: SearcherPageViewModel.ViewState, + onValueChange: (String) -> Unit +) { + Scaffold(modifier = Modifier.fillMaxSize()) { + with(viewState) { + LazyColumn(modifier = Modifier + .fillMaxSize() + .padding(it)) { + item { + QueryTextBox( + modifier = Modifier.padding(horizontal = 8.dp), + query = query, + onValueChange = { query -> + onValueChange(query) + } + ) + } + + if (searchResult.tracks != null) { + items(searchResult.tracks!!.size) { track -> + with(searchResult.tracks!![track]){ + var artists: List = this.artists.map { artist -> artist.name } + SearchingSongComponent(artworkUrl = album.images[0].url, songName = this.name, artists = artists.joinToString(", "), spotifyUrl = this.externalUrls.spotify ?: "") + } + } + } + } + } + } +} + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) +@Composable +fun QueryTextBox( + modifier: Modifier = Modifier, + query: String, + onValueChange: (String) -> Unit +) { + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + val softwareKeyboardController = LocalSoftwareKeyboardController.current + + OutlinedTextField( + value = query, + onValueChange = onValueChange, + label = { Text(text = stringResource(id = R.string.searcher_page_query_text_box_label)) }, + modifier = modifier + .fillMaxWidth() + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search + ), + keyboardActions = KeyboardActions( + onSearch = { + focusManager.clearFocus() + softwareKeyboardController?.hide() + } + ), + leadingIcon = { + Icon(imageVector = Icons.Rounded.Search, contentDescription = null) + }, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton(onClick = { onValueChange("") }) { + Icon(imageVector = Icons.Rounded.Close, contentDescription = null) + } + } + }, + singleLine = true, + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + 8.dp + ), unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt new file mode 100644 index 00000000..98b1672e --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt @@ -0,0 +1,53 @@ +package com.bobbyesp.spowlo.ui.pages.searcher + +import android.util.Log +import androidx.lifecycle.ViewModel +import com.adamratzman.spotify.models.SpotifySearchResult +import com.bobbyesp.spowlo.features.spotify_api.SpotifyApiRequests +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +class SearcherPageViewModel @Inject constructor() : ViewModel() { + + private val mutableViewStateFlow = MutableStateFlow(ViewState()) + val viewStateFlow = mutableViewStateFlow.asStateFlow() + + data class ViewState( + val query : String = "", + val isSearchError : Boolean = false, + val isSearching : Boolean = false, + val searchResult : SpotifySearchResult = SpotifySearchResult() + ) + + private val api = SpotifyApiRequests + + fun updateSearchText(text: String) { + mutableViewStateFlow.update { + it.copy(query = text) + } + } + + suspend fun makeSearch() { + mutableViewStateFlow.update { + it.copy(isSearching = true) + } + kotlin.runCatching { + api.searchAllTypes(viewStateFlow.value.query) + }.onSuccess { result -> + mutableViewStateFlow.update { viewState -> + viewState.copy(searchResult = result) + } + Log.d("SearcherPageViewModel", "makeSearch: $result") + }.onFailure { + mutableViewStateFlow.update { viewState -> + viewState.copy(isSearchError = true) + } + it.printStackTrace() + }.also { + mutableViewStateFlow.update { + it.copy(isSearching = false) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79f80822..7e171d5f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -259,4 +259,6 @@ Geo bypass Use a localization bypass to download songs from countries that YT Music is restricted. An error ocurred while trying to connect to the Spotify API + What would you like to download? + No results were found \ No newline at end of file From b18fb04b561c30a0ab5c7f9e441b408693d9269f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sun, 12 Mar 2023 00:47:05 +0100 Subject: [PATCH 06/42] beauty: Added a horizontal divider for each song object --- .../spowlo/ui/pages/searcher/SearcherPage.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 5ee87383..5c96e168 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -2,6 +2,7 @@ package com.bobbyesp.spowlo.ui.pages.searcher import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -26,6 +27,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager @@ -36,7 +38,9 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.songs.search_feat.SearchingSongComponent +import com.bobbyesp.spowlo.ui.theme.harmonizeWithPrimary import kotlinx.coroutines.delay @Composable @@ -75,6 +79,9 @@ fun SearcherPageImpl( LazyColumn(modifier = Modifier .fillMaxSize() .padding(it)) { + item { + Spacer(modifier = Modifier.padding(vertical = 6.dp)) + } item { QueryTextBox( modifier = Modifier.padding(horizontal = 8.dp), @@ -84,13 +91,19 @@ fun SearcherPageImpl( } ) } - + item { + Spacer(modifier = Modifier.padding(vertical = 8.dp)) + } if (searchResult.tracks != null) { items(searchResult.tracks!!.size) { track -> with(searchResult.tracks!![track]){ var artists: List = this.artists.map { artist -> artist.name } SearchingSongComponent(artworkUrl = album.images[0].url, songName = this.name, artists = artists.joinToString(", "), spotifyUrl = this.externalUrls.spotify ?: "") } + //if it is not the last item, add a horizontal divider + if (track != searchResult.tracks!!.size - 1) { + HorizontalDivider(modifier = Modifier.alpha(0.45f), color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary()) + } } } } From 4386c7c193f84bc4b70751e7174ba95da668a01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sun, 12 Mar 2023 01:42:09 +0100 Subject: [PATCH 07/42] beauty: Updated songs searched component --- .../search_feat/SearchingSongComponent.kt | 15 ++++---- .../spowlo/ui/pages/searcher/SearcherPage.kt | 36 ++++++++++++------- app/src/main/res/values/strings.xml | 1 + 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt index 3e3e7301..35426315 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.bobbyesp.spowlo.App +import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.AsyncImageImpl import com.bobbyesp.spowlo.ui.components.MarqueeText import com.bobbyesp.spowlo.utils.ChromeCustomTabsUtil @@ -29,7 +31,8 @@ fun SearchingSongComponent( artworkUrl: String, songName: String, artists: String, - spotifyUrl: String + spotifyUrl: String, + type : String = App.context.getString(R.string.single) ) { Column(Modifier.fillMaxWidth().clickable { ChromeCustomTabsUtil.openUrl(spotifyUrl) }.padding(8.dp)) { Row( @@ -39,7 +42,7 @@ fun SearchingSongComponent( AsyncImageImpl( modifier = Modifier .padding(horizontal = 12.dp, vertical = 6.dp) - .size(72.dp) + .size(45.dp) .aspectRatio(1f, matchHeightConstraintsFirst = true) .clip(MaterialTheme.shapes.small), model = artworkUrl, @@ -68,20 +71,20 @@ fun SearchingSongComponent( color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, - fontSize = 16.sp, + fontSize = 15.sp, fontWeight = FontWeight.Bold, basicGradientColor = MaterialTheme.colorScheme.surface.copy( alpha = 0.8f ), ) } - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(6.dp)) MarqueeText( - text = artists, + text = "$artists • $type", color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), maxLines = 1, overflow = TextOverflow.Ellipsis, - fontSize = 12.sp, + fontSize = 10.sp, basicGradientColor = MaterialTheme.colorScheme.surface.copy( alpha = 0.8f ), diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 5c96e168..43a1c25a 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -76,15 +76,14 @@ fun SearcherPageImpl( ) { Scaffold(modifier = Modifier.fillMaxSize()) { with(viewState) { - LazyColumn(modifier = Modifier - .fillMaxSize() - .padding(it)) { - item { - Spacer(modifier = Modifier.padding(vertical = 6.dp)) - } + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it) + ) { item { QueryTextBox( - modifier = Modifier.padding(horizontal = 8.dp), + modifier = Modifier.padding(), query = query, onValueChange = { query -> onValueChange(query) @@ -92,17 +91,25 @@ fun SearcherPageImpl( ) } item { - Spacer(modifier = Modifier.padding(vertical = 8.dp)) + Spacer(modifier = Modifier.padding(vertical = 4.dp)) } if (searchResult.tracks != null) { items(searchResult.tracks!!.size) { track -> - with(searchResult.tracks!![track]){ + with(searchResult.tracks!![track]) { var artists: List = this.artists.map { artist -> artist.name } - SearchingSongComponent(artworkUrl = album.images[0].url, songName = this.name, artists = artists.joinToString(", "), spotifyUrl = this.externalUrls.spotify ?: "") + SearchingSongComponent( + artworkUrl = album.images[0].url, + songName = this.name, + artists = artists.joinToString(", "), + spotifyUrl = this.externalUrls.spotify ?: "" + ) } //if it is not the last item, add a horizontal divider if (track != searchResult.tracks!!.size - 1) { - HorizontalDivider(modifier = Modifier.alpha(0.45f), color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary()) + HorizontalDivider( + modifier = Modifier.alpha(0.45f), + color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) } } } @@ -125,8 +132,13 @@ fun QueryTextBox( OutlinedTextField( value = query, onValueChange = onValueChange, - label = { Text(text = stringResource(id = R.string.searcher_page_query_text_box_label)) }, + placeholder = { + if (query.isEmpty()) { + Text(text = stringResource(id = R.string.searcher_page_query_text_box_label)) + } + }, modifier = modifier + .padding(16.dp) .fillMaxWidth() .focusRequester(focusRequester), keyboardOptions = KeyboardOptions( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e171d5f..5564d3eb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -261,4 +261,5 @@ An error ocurred while trying to connect to the Spotify API What would you like to download? No results were found + Single \ No newline at end of file From a542146be302ca8763bda44a2b1f5ced3407fef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sun, 12 Mar 2023 10:54:16 +0100 Subject: [PATCH 08/42] bugfix: Navigation splitted in navigation() extensions --- .../java/com/bobbyesp/spowlo/MainActivity.kt | 4 ++-- .../com/bobbyesp/spowlo/ui/common/Route.kt | 4 ++++ .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 21 ++++++++++++------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt b/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt index 4ff3a486..51708341 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt @@ -121,8 +121,8 @@ class MainActivity : AppCompatActivity() { } val showInBottomNavigation = mapOf( - Route.HOME to Icons.Rounded.Download, - Route.SEARCHER to Icons.Rounded.Search, + Route.DownloaderNavi to Icons.Rounded.Download, + Route.SearcherNavi to Icons.Rounded.Search, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt index 70ee8094..0dd8f89b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt @@ -3,6 +3,10 @@ package com.bobbyesp.spowlo.ui.common object Route { const val NavGraph = "nav_graph" + const val SearcherNavi = "searcher_navi" + const val DownloaderNavi = "downloader_navi" + + const val HOME = "home" const val DOWNLOADER = "downloader" const val DOWNLOADS_HISTORY = "download_history" diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index f89f5401..0455afb5 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -52,6 +52,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.dialog import androidx.navigation.navArgument import androidx.navigation.navDeepLink +import androidx.navigation.navigation import com.bobbyesp.library.SpotDL import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.BuildConfig @@ -124,7 +125,7 @@ fun InitialEntry( val currentRootRoute = remember(navBackStackEntry) { mutableStateOf( - navController.currentBackStack.value.getOrNull(1)?.destination?.route, + navBackStackEntry?.destination?.parent?.route ?: Route.DownloaderNavi ) } @@ -207,8 +208,8 @@ fun InitialEntry( ) { MainActivity.showInBottomNavigation.forEach { (route, icon) -> val text = when (route) { - Route.HOME -> App.context.getString(R.string.downloader) - Route.SEARCHER -> App.context.getString(R.string.searcher) + Route.DownloaderNavi -> App.context.getString(R.string.downloader) + Route.SearcherNavi -> App.context.getString(R.string.searcher) else -> "" } @@ -263,9 +264,10 @@ fun InitialEntry( .align(Alignment.Center) .padding(bottom = paddingValues.calculateBottomPadding()), navController = navController, - startDestination = Route.HOME, + startDestination = Route.DownloaderNavi, route = Route.NavGraph ) { + navigation(startDestination = Route.HOME, route = Route.DownloaderNavi) { //TODO: Add all routes animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME DownloaderPage( @@ -365,10 +367,6 @@ fun InitialEntry( } } - animatedComposableVariant(Route.SEARCHER) { - SearcherPage( - ) - } navDeepLink { // Want to go to "markdown_viewer/{markdownFileName}" @@ -406,8 +404,15 @@ fun InitialEntry( navController ) } + } //Can add the downloads history bottom sheet here using `val downloadsHistoryViewModel = hiltViewModel()` + navigation(startDestination = Route.SEARCHER, route = Route.SearcherNavi){ + animatedComposableVariant(Route.SEARCHER) { + SearcherPage( + ) + } + } } } } From 1bec6d957d1778fc71b0c32eaf9927430a097247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sun, 12 Mar 2023 23:32:56 +0100 Subject: [PATCH 09/42] feat: Started adding the playlist/track... viewer page --- .../spotify_api/SpotifyApiRequests.kt | 19 +- .../data/binders/SpotifyItemBinder.kt | 2 + .../spotify_api/data/dtos/SpotifyData.kt | 15 + .../com/bobbyesp/spowlo/ui/common/Route.kt | 2 + .../search_feat/SearchingSongComponent.kt | 9 +- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 287 ++++++++++-------- .../spowlo/ui/pages/common/LoadingPage.kt | 44 +++ .../metadata_viewer/playlists/PlaylistPage.kt | 87 ++++++ .../playlists/PlaylistPageViewModel.kt | 26 ++ .../spowlo/ui/pages/searcher/SearcherPage.kt | 14 +- app/src/main/res/values/strings.xml | 1 + 11 files changed, 374 insertions(+), 132 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/binders/SpotifyItemBinder.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/LoadingPage.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt index 10445b7a..913081ea 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt @@ -2,6 +2,7 @@ package com.bobbyesp.spowlo.features.spotify_api import android.util.Log import com.adamratzman.spotify.SpotifyAppApi +import com.adamratzman.spotify.models.Playlist import com.adamratzman.spotify.models.SpotifyPublicUser import com.adamratzman.spotify.models.SpotifySearchResult import com.adamratzman.spotify.models.Token @@ -23,7 +24,10 @@ object SpotifyApiRequests { //Pulls the clientId and clientSecret tokens and builds them into an object suspend fun buildApi() { - Log.d("SpotifyApiRequests", "Building API with client ID: $clientId and client secret: $clientSecret") + Log.d( + "SpotifyApiRequests", + "Building API with client ID: $clientId and client secret: $clientSecret" + ) token = spotifyAppApi(clientId, clientSecret).build().token api = spotifyAppApi(clientId, clientSecret, token!!) { automaticRefresh = true @@ -59,4 +63,17 @@ object SpotifyApiRequests { } return SpotifySearchResult() } + + //search by id + suspend fun searchPlaylistById(id: String): Playlist? { + kotlin.runCatching { + api!!.playlists.getPlaylist(id, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return null + }.onSuccess { + return it + } + return null + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/binders/SpotifyItemBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/binders/SpotifyItemBinder.kt new file mode 100644 index 00000000..e6568a56 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/binders/SpotifyItemBinder.kt @@ -0,0 +1,2 @@ +package com.bobbyesp.spowlo.features.spotify_api.data.binders + diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt new file mode 100644 index 00000000..12eafaba --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt @@ -0,0 +1,15 @@ +package com.bobbyesp.spowlo.features.spotify_api.data.dtos + +data class SpotifyData( + val artworkUrl: String = "", + val name: String = "", + val artists: List = emptyList(), + val type: SpotifyDataType = SpotifyDataType.TRACK +) + +enum class SpotifyDataType { + TRACK, + ALBUM, + PLAYLIST, + ARTIST +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt index 0dd8f89b..5bf263a5 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt @@ -26,6 +26,8 @@ object Route { const val MORE_OPTIONS_HOME = "more_options_home" const val SONG_INFO_HISTORY = "song_info_history" + const val PLAYLIST_PAGE = "playlist_page" + const val APPEARANCE = "appearance" const val APP_THEME = "app_theme" const val GENERAL_DOWNLOAD_PREFERENCES = "general_download_preferences" diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt index 35426315..154d308f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/search_feat/SearchingSongComponent.kt @@ -32,9 +32,14 @@ fun SearchingSongComponent( songName: String, artists: String, spotifyUrl: String, - type : String = App.context.getString(R.string.single) + type : String = App.context.getString(R.string.single), + onClick: () -> Unit = { ChromeCustomTabsUtil.openUrl(spotifyUrl)} ) { - Column(Modifier.fillMaxWidth().clickable { ChromeCustomTabsUtil.openUrl(spotifyUrl) }.padding(8.dp)) { + Column( + Modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(8.dp)) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, //This makes all go to the center diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 0455afb5..f86d0a6e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -70,6 +70,7 @@ import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.MoreOptionsHomeBottomSheet import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderPage import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel import com.bobbyesp.spowlo.ui.pages.history.DownloadsHistoryPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists.PlaylistPage import com.bobbyesp.spowlo.ui.pages.mod_downloader.ModsDownloaderPage import com.bobbyesp.spowlo.ui.pages.mod_downloader.ModsDownloaderViewModel import com.bobbyesp.spowlo.ui.pages.playlist.PlaylistMetadataPage @@ -186,6 +187,7 @@ fun InitialEntry( .fillMaxSize() .background(MaterialTheme.colorScheme.background) ) { + val navRootUrl = "android-app://androidx.navigation/" ModalBottomSheetLayout( bottomSheetNavigator, sheetShape = MaterialTheme.shapes.medium.copy( @@ -197,7 +199,8 @@ fun InitialEntry( ) { Scaffold( bottomBar = { - AnimatedVisibility(visible = !shouldHideBottomNavBar, + AnimatedVisibility( + visible = !shouldHideBottomNavBar, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { @@ -268,150 +271,184 @@ fun InitialEntry( route = Route.NavGraph ) { navigation(startDestination = Route.HOME, route = Route.DownloaderNavi) { - //TODO: Add all routes - animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME - DownloaderPage( - navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY) }, - navigateToSettings = { navController.navigate(Route.MORE_OPTIONS_HOME) }, - navigateToPlaylistPage = { navController.navigate(Route.PLAYLIST) }, - onSongCardClicked = { - navController.navigate(Route.PLAYLIST_METADATA_PAGE) - }, - onNavigateToTaskList = { navController.navigate(Route.TASK_LIST) }, - navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) }, - downloaderViewModel = downloaderViewModel - ) - } - animatedComposable(Route.SETTINGS) { - SettingsPage( - navController = navController - ) - } - animatedComposable(Route.GENERAL_DOWNLOAD_PREFERENCES) { - GeneralSettingsPage( - onBackPressed = onBackPressed - ) - } - animatedComposable(Route.DOWNLOADS_HISTORY) { - DownloadsHistoryPage( - onBackPressed = onBackPressed, - ) - } - animatedComposable(Route.DOWNLOAD_DIRECTORY) { - DownloadsDirectoriesPage { - onBackPressed() + //TODO: Add all routes + animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME + DownloaderPage( + navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY) }, + navigateToSettings = { navController.navigate(Route.MORE_OPTIONS_HOME) }, + navigateToPlaylistPage = { navController.navigate(Route.PLAYLIST) }, + onSongCardClicked = { + navController.navigate(Route.PLAYLIST_METADATA_PAGE) + }, + onNavigateToTaskList = { navController.navigate(Route.TASK_LIST) }, + navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) }, + downloaderViewModel = downloaderViewModel + ) } - } - animatedComposable(Route.APPEARANCE) { - AppearancePage(navController = navController) - } - animatedComposable(Route.APP_THEME) { - AppThemePreferencesPage { - onBackPressed() + animatedComposable(Route.SETTINGS) { + SettingsPage( + navController = navController + ) } - } - animatedComposable(Route.DOWNLOAD_FORMAT) { - SettingsFormatsPage { - onBackPressed() + animatedComposable(Route.GENERAL_DOWNLOAD_PREFERENCES) { + GeneralSettingsPage( + onBackPressed = onBackPressed + ) } - } - animatedComposable(Route.SPOTIFY_PREFERENCES) { - SpotifySettingsPage { - onBackPressed() + animatedComposable(Route.DOWNLOADS_HISTORY) { + DownloadsHistoryPage( + onBackPressed = onBackPressed, + ) + } + animatedComposable(Route.DOWNLOAD_DIRECTORY) { + DownloadsDirectoriesPage { + onBackPressed() + } + } + animatedComposable(Route.APPEARANCE) { + AppearancePage(navController = navController) + } + animatedComposable(Route.APP_THEME) { + AppThemePreferencesPage { + onBackPressed() + } + } + animatedComposable(Route.DOWNLOAD_FORMAT) { + SettingsFormatsPage { + onBackPressed() + } + } + animatedComposable(Route.SPOTIFY_PREFERENCES) { + SpotifySettingsPage { + onBackPressed() + } + } + slideInVerticallyComposable(Route.PLAYLIST_METADATA_PAGE) { + PlaylistMetadataPage( + onBackPressed, + //TODO: ADD THE ABILITY TO PASS JUST SONGS AND NOT GET THEM FROM THE MUTABLE STATE + ) + } + animatedComposable(Route.MODS_DOWNLOADER) { + ModsDownloaderPage( + onBackPressed, + modsDownloaderViewModel + ) + } + animatedComposable(Route.COOKIE_PROFILE) { + CookieProfilePage( + cookiesViewModel = cookiesViewModel, + navigateToCookieGeneratorPage = { navController.navigate(Route.COOKIE_GENERATOR_WEBVIEW) }, + ) { onBackPressed() } + } + animatedComposable( + Route.COOKIE_GENERATOR_WEBVIEW + ) { + WebViewPage(cookiesViewModel) { onBackPressed() } + } + animatedComposable(Route.UPDATER_PAGE) { + UpdaterPage( + onBackPressed + ) + } + animatedComposable(Route.DOCUMENTATION) { + DocumentationPage( + onBackPressed, + navController + ) } - } - slideInVerticallyComposable(Route.PLAYLIST_METADATA_PAGE) { - PlaylistMetadataPage( - onBackPressed, - //TODO: ADD THE ABILITY TO PASS JUST SONGS AND NOT GET THEM FROM THE MUTABLE STATE - ) - } - animatedComposable(Route.MODS_DOWNLOADER) { - ModsDownloaderPage( - onBackPressed, - modsDownloaderViewModel - ) - } - animatedComposable(Route.COOKIE_PROFILE) { - CookieProfilePage( - cookiesViewModel = cookiesViewModel, - navigateToCookieGeneratorPage = { navController.navigate(Route.COOKIE_GENERATOR_WEBVIEW) }, - ) { onBackPressed() } - } - animatedComposable( - Route.COOKIE_GENERATOR_WEBVIEW - ) { - WebViewPage(cookiesViewModel) { onBackPressed() } - } - animatedComposable(Route.UPDATER_PAGE) { - UpdaterPage( - onBackPressed - ) - } - animatedComposable(Route.DOCUMENTATION) { - DocumentationPage( - onBackPressed, - navController - ) - } - animatedComposable(Route.ABOUT) { - AboutPage { - onBackPressed() + animatedComposable(Route.ABOUT) { + AboutPage { + onBackPressed() + } } - } - animatedComposable(Route.LANGUAGES) { - LanguagePage { - onBackPressed() + animatedComposable(Route.LANGUAGES) { + LanguagePage { + onBackPressed() + } } - } - navDeepLink { - // Want to go to "markdown_viewer/{markdownFileName}" - uriPattern = - "android-app://androidx.navigation/markdown_viewer/{markdownFileName}" - } + navDeepLink { + // Want to go to "markdown_viewer/{markdownFileName}" + uriPattern = + "android-app://androidx.navigation/markdown_viewer/{markdownFileName}" + } - animatedComposable( - "markdown_viewer/{markdownFileName}", - arguments = listOf(navArgument("markdownFileName") { - type = NavType.StringType - }) - ) { backStackEntry -> - val mdFileName = - backStackEntry.arguments?.getString("markdownFileName") ?: "" - Log.d("MainActivity", mdFileName) - MarkdownViewerPage( - markdownFileName = mdFileName, - onBackPressed = onBackPressed - ) - } + animatedComposable( + "markdown_viewer/{markdownFileName}", + arguments = listOf( + navArgument( + "markdownFileName" + ) { + type = NavType.StringType + } + ) + ) { backStackEntry -> + val mdFileName = + backStackEntry.arguments?.getString("markdownFileName") ?: "" + Log.d("MainActivity", mdFileName) + MarkdownViewerPage( + markdownFileName = mdFileName, + onBackPressed = onBackPressed + ) + } - //DIALOGS ------------------------------- - //TODO: ADD DIALOGS - dialog(Route.AUDIO_QUALITY_DIALOG) { - AudioQualityDialog( - onBackPressed - ) - } + //DIALOGS ------------------------------- + //TODO: ADD DIALOGS + dialog(Route.AUDIO_QUALITY_DIALOG) { + AudioQualityDialog( + onBackPressed + ) + } - //BOTTOM SHEETS -------------------------- - bottomSheet(Route.MORE_OPTIONS_HOME) { - MoreOptionsHomeBottomSheet( - onBackPressed, - navController - ) + //BOTTOM SHEETS -------------------------- + bottomSheet(Route.MORE_OPTIONS_HOME) { + MoreOptionsHomeBottomSheet( + onBackPressed, + navController + ) + } } - } //Can add the downloads history bottom sheet here using `val downloadsHistoryViewModel = hiltViewModel()` - navigation(startDestination = Route.SEARCHER, route = Route.SearcherNavi){ + navigation(startDestination = Route.SEARCHER, route = Route.SearcherNavi) { animatedComposableVariant(Route.SEARCHER) { SearcherPage( + navController = navController ) } + + + //create a deeplink to the playlist page passing the id of the playlist + navDeepLink { + // Want to go to "markdown_viewer/{markdownFileName}" + uriPattern = + StringBuilder().append(navRootUrl).append(Route.PLAYLIST_PAGE) + .append("/{id}").toString() + Log.d("TST_NAV", uriPattern!!) + } + + val navArgument = navArgument("id") { + type = NavType.StringType + } + val routeWithIdPattern: String = + StringBuilder().append(Route.PLAYLIST_PAGE).append("/{id}").toString() + animatedComposableVariant( + routeWithIdPattern, + arguments = listOf(navArgument) + ) { backStackEntry -> + val id = + backStackEntry.arguments?.getString("id") ?: "SOMETHING WENT WRONG" + PlaylistPage( + onBackPressed, + id = id + ) + } + + } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/LoadingPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/LoadingPage.kt new file mode 100644 index 00000000..9a875e24 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/LoadingPage.kt @@ -0,0 +1,44 @@ +package com.bobbyesp.spowlo.ui.pages.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +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.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.bobbyesp.spowlo.R + +@Composable +fun LoadingPage() { + //create a loading page + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + modifier = Modifier + .size(72.dp) + .padding(6.dp), + strokeWidth = 4.dp + ) + Text(text = stringResource(id = R.string.page_loading), modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt new file mode 100644 index 00000000..f13bb99d --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt @@ -0,0 +1,87 @@ +package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.ui.components.BackButton +import com.bobbyesp.spowlo.ui.pages.common.LoadingPage +import kotlinx.coroutines.delay + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PlaylistPage( + onBackPressed: () -> Unit, + playlistPageViewModel: PlaylistPageViewModel = hiltViewModel(), + id: String +) { + val scope = rememberCoroutineScope() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + val viewState by playlistPageViewModel.viewStateFlow.collectAsStateWithLifecycle() + + LaunchedEffect(Unit){ + delay(2000) + playlistPageViewModel.loadData() + } + + with(viewState) { + when (this.state) { + is PlaylistDataState.Loading -> { + LoadingPage() + } + + is PlaylistDataState.Error -> { + Text(text = this.state.error.message.toString()) + } + + is PlaylistDataState.Loaded -> { + Scaffold(modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar(title = { + Text( + text = "Playlist (WIP)", + style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp) + ) + }, navigationIcon = { + BackButton { onBackPressed() } + }, actions = { + }, scrollBehavior = scrollBehavior + ) + }) { paddings -> + Text(text = this.state.data.toString(), modifier = Modifier.padding(paddings)) + } + + } + + } + } + +} + +sealed class PlaylistDataState { + object Loading : PlaylistDataState() + class Error(val error: Exception) : PlaylistDataState() + class Loaded(val data: SpotifyData) : PlaylistDataState() +} + +class ToolbarOptions( + val big: Boolean = false, + val alwaysVisible: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt new file mode 100644 index 00000000..f9c334ae --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt @@ -0,0 +1,26 @@ +package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists + +import androidx.lifecycle.ViewModel +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyDataType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +class PlaylistPageViewModel @Inject constructor() : ViewModel() { + + private val mutableViewStateFlow = MutableStateFlow(ViewState()) + val viewStateFlow = mutableViewStateFlow.asStateFlow() + + data class ViewState( + val id : String = "", + val state : PlaylistDataState = PlaylistDataState.Loading, + ) + + suspend fun loadData(){ + mutableViewStateFlow.update { + it.copy(state = PlaylistDataState.Loaded(SpotifyData("", "Faded", listOf("Alan Walker"), SpotifyDataType.TRACK))) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 43a1c25a..3fe1e07e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -37,7 +37,9 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.songs.search_feat.SearchingSongComponent import com.bobbyesp.spowlo.ui.theme.harmonizeWithPrimary @@ -45,7 +47,8 @@ import kotlinx.coroutines.delay @Composable fun SearcherPage( - searcherPageViewModel: SearcherPageViewModel = hiltViewModel() + searcherPageViewModel: SearcherPageViewModel = hiltViewModel(), + navController: NavController ) { val viewState by searcherPageViewModel.viewStateFlow.collectAsStateWithLifecycle() @@ -58,7 +61,8 @@ fun SearcherPage( viewState = viewState, onValueChange = { query -> searcherPageViewModel.updateSearchText(query) - } + }, + onItemClick = { navController.navigate(Route.PLAYLIST_PAGE + "/" + "thisisjustatest")} ) } LaunchedEffect(viewState.query) { @@ -72,7 +76,8 @@ fun SearcherPage( @Composable fun SearcherPageImpl( viewState: SearcherPageViewModel.ViewState, - onValueChange: (String) -> Unit + onValueChange: (String) -> Unit, + onItemClick : () -> Unit ) { Scaffold(modifier = Modifier.fillMaxSize()) { with(viewState) { @@ -101,7 +106,8 @@ fun SearcherPageImpl( artworkUrl = album.images[0].url, songName = this.name, artists = artists.joinToString(", "), - spotifyUrl = this.externalUrls.spotify ?: "" + spotifyUrl = this.externalUrls.spotify ?: "", + onClick = onItemClick ) } //if it is not the last item, add a horizontal divider diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5564d3eb..630b2b1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -262,4 +262,5 @@ What would you like to download? No results were found Single + Loading the page... \ No newline at end of file From 12881f84feb803c5078816e7b14d8283502545fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 13 Mar 2023 21:15:16 +0100 Subject: [PATCH 10/42] feat: Added page states and searching improvements. Fixed imePaddings --- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 7 +- .../spowlo/ui/pages/common/ErrorPage.kt | 69 ++++++++ .../ui/pages/downloader/DownloaderPage.kt | 2 +- .../metadata_viewer/playlists/PlaylistPage.kt | 4 +- .../spowlo/ui/pages/searcher/SearcherPage.kt | 164 ++++++++++++++---- .../pages/searcher/SearcherPageViewModel.kt | 22 +-- .../settings/format/SettingsFormatsPage.kt | 6 +- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 46 +++-- .../bobbyesp/spowlo/utils/PreferencesUtil.kt | 52 +++--- app/src/main/res/values/strings.xml | 7 + 10 files changed, 277 insertions(+), 102 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index f86d0a6e..df8d7057 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -17,6 +17,8 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding @@ -111,7 +113,7 @@ private const val TAG = "InitialEntry" @OptIn( ExperimentalAnimationApi::class, ExperimentalMaterialNavigationApi::class, - ExperimentalMaterial3Api::class + ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class ) @Composable fun InitialEntry( @@ -265,7 +267,8 @@ fun InitialEntry( } ) .align(Alignment.Center) - .padding(bottom = paddingValues.calculateBottomPadding()), + .padding(paddingValues) + .consumeWindowInsets(paddingValues), navController = navController, startDestination = Route.DownloaderNavi, route = Route.NavGraph diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt new file mode 100644 index 00000000..25bafbd7 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt @@ -0,0 +1,69 @@ +package com.bobbyesp.spowlo.ui.pages.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Error +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import com.bobbyesp.spowlo.R + +@Composable +fun ErrorPage( + onReload: () -> Unit, + exception: String, + modifier: Modifier +) { + val clipboard = LocalClipboardManager.current + + Box(modifier) { + Column( + Modifier + .align(Alignment.Center) + ) { + Icon( + Icons.Rounded.Error, contentDescription = null, modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(56.dp) + .padding(bottom = 12.dp) + ) + Text( + stringResource(id = R.string.error), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp) + ) { + OutlinedButton( + onClick = { + clipboard.setText(AnnotatedString("Message: ${exception}\n\n")) + }) { + Text(stringResource(id = R.string.error_copy)) + } + + Spacer(modifier = Modifier.width(8.dp)) + + OutlinedButton( + onClick = { onReload() }) { + Text(stringResource(id = R.string.err_act_reload)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index 96d26929..2b8e6e19 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -261,7 +261,7 @@ fun DownloaderPageImplementation( }) }, floatingActionButton = { FABs( - modifier = with(receiver = Modifier) { if (showDownloadProgress) this else this.imePadding() }, + modifier = with(Modifier) { if (showDownloadProgress) this else this.imePadding() }, downloadCallback = downloadCallback, pasteCallback = pasteCallback, cancelCallback = cancelCallback, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt index f13bb99d..60ce06fe 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt @@ -35,7 +35,7 @@ fun PlaylistPage( val viewState by playlistPageViewModel.viewStateFlow.collectAsStateWithLifecycle() LaunchedEffect(Unit){ - delay(2000) + delay(1000) playlistPageViewModel.loadData() } @@ -65,7 +65,7 @@ fun PlaylistPage( }, scrollBehavior = scrollBehavior ) }) { paddings -> - Text(text = this.state.data.toString(), modifier = Modifier.padding(paddings)) + Text(text = this.state.data.toString() + id, modifier = Modifier.padding(paddings)) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 3fe1e07e..5731b913 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -1,17 +1,20 @@ package com.bobbyesp.spowlo.ui.pages.searcher import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -25,6 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -33,6 +37,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -62,7 +67,7 @@ fun SearcherPage( onValueChange = { query -> searcherPageViewModel.updateSearchText(query) }, - onItemClick = { navController.navigate(Route.PLAYLIST_PAGE + "/" + "thisisjustatest")} + onItemClick = { id -> navController.navigate(Route.PLAYLIST_PAGE + "/" + id) } ) } LaunchedEffect(viewState.query) { @@ -77,45 +82,130 @@ fun SearcherPage( fun SearcherPageImpl( viewState: SearcherPageViewModel.ViewState, onValueChange: (String) -> Unit, - onItemClick : () -> Unit + onItemClick: (String) -> Unit ) { Scaffold(modifier = Modifier.fillMaxSize()) { with(viewState) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(it) - ) { - item { - QueryTextBox( - modifier = Modifier.padding(), - query = query, - onValueChange = { query -> - onValueChange(query) + Column(modifier = Modifier.fillMaxSize()) { + QueryTextBox( + modifier = Modifier.padding(), + query = query, + onValueChange = { query -> + onValueChange(query) + } + ) + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it) + ) { + when (viewState.viewState) { + is ViewSearchState.Idle -> { + item { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.search), + modifier = Modifier.align( + Alignment.CenterHorizontally + ), + style = MaterialTheme.typography.displaySmall, + fontWeight = FontWeight.Bold + ) + } + + } + } } - ) - } - item { - Spacer(modifier = Modifier.padding(vertical = 4.dp)) - } - if (searchResult.tracks != null) { - items(searchResult.tracks!!.size) { track -> - with(searchResult.tracks!![track]) { - var artists: List = this.artists.map { artist -> artist.name } - SearchingSongComponent( - artworkUrl = album.images[0].url, - songName = this.name, - artists = artists.joinToString(", "), - spotifyUrl = this.externalUrls.spotify ?: "", - onClick = onItemClick - ) + + is ViewSearchState.Loading -> { + item { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + modifier = Modifier + .size(72.dp) + .padding(6.dp), + strokeWidth = 4.dp + ) + Text( + text = stringResource(id = R.string.loading), + modifier = Modifier.align( + Alignment.CenterHorizontally + ), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + } + + } + } + } + + is ViewSearchState.Error -> { + item { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.error), + modifier = Modifier.align( + Alignment.CenterHorizontally + ), + style = MaterialTheme.typography.displaySmall, + fontWeight = FontWeight.Bold + ) + } + } + } } - //if it is not the last item, add a horizontal divider - if (track != searchResult.tracks!!.size - 1) { - HorizontalDivider( - modifier = Modifier.alpha(0.45f), - color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() - ) + + is ViewSearchState.Success -> { + if (viewState.viewState.data.tracks != null) { + items(viewState.viewState.data.tracks!!.size) { track -> + with(viewState.viewState.data.tracks!![track]) { + val artists: List = + this.artists.map { artist -> artist.name } + SearchingSongComponent( + artworkUrl = album.images[0].url, + songName = this.name, + artists = artists.joinToString(", "), + spotifyUrl = this.externalUrls.spotify ?: "", + onClick = { onItemClick(this.id) } + ) + } + //if it is not the last item, add a horizontal divider + if (track != viewState.viewState.data.tracks!!.size - 1) { + HorizontalDivider( + modifier = Modifier.alpha(0.35f), + color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) + } + } + } } } } @@ -144,7 +234,7 @@ fun QueryTextBox( } }, modifier = modifier - .padding(16.dp) + .padding(top = 16.dp, bottom = 4.dp, start = 16.dp, end = 16.dp) .fillMaxWidth() .focusRequester(focusRequester), keyboardOptions = KeyboardOptions( diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt index 98b1672e..1e61705c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt @@ -15,9 +15,7 @@ class SearcherPageViewModel @Inject constructor() : ViewModel() { data class ViewState( val query : String = "", - val isSearchError : Boolean = false, - val isSearching : Boolean = false, - val searchResult : SpotifySearchResult = SpotifySearchResult() + val viewState: ViewSearchState = ViewSearchState.Idle, ) private val api = SpotifyApiRequests @@ -30,24 +28,28 @@ class SearcherPageViewModel @Inject constructor() : ViewModel() { suspend fun makeSearch() { mutableViewStateFlow.update { - it.copy(isSearching = true) + it.copy(viewState = ViewSearchState.Loading) } kotlin.runCatching { api.searchAllTypes(viewStateFlow.value.query) }.onSuccess { result -> mutableViewStateFlow.update { viewState -> - viewState.copy(searchResult = result) + viewState.copy(viewState = ViewSearchState.Success(result)) } Log.d("SearcherPageViewModel", "makeSearch: $result") }.onFailure { mutableViewStateFlow.update { viewState -> - viewState.copy(isSearchError = true) + viewState.copy(viewState = ViewSearchState.Error(it.message.toString())) } it.printStackTrace() - }.also { - mutableViewStateFlow.update { - it.copy(isSearching = false) - } } } +} + +//create the possible states of the view +sealed class ViewSearchState { + object Idle : ViewSearchState() + object Loading : ViewSearchState() + data class Success(val data: SpotifySearchResult) : ViewSearchState() + data class Error(val error: String) : ViewSearchState() } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt index d3dc8844..35586eb5 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt @@ -7,15 +7,16 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AudioFile import androidx.compose.material.icons.outlined.Audiotrack import androidx.compose.material.icons.outlined.HighQuality +import androidx.compose.material.icons.outlined.ShuffleOn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -23,7 +24,6 @@ import androidx.compose.ui.res.stringResource import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.LargeTopAppBar -import com.bobbyesp.spowlo.ui.components.PreferenceInfo import com.bobbyesp.spowlo.ui.components.PreferenceItem import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle import com.bobbyesp.spowlo.ui.components.PreferenceSwitch @@ -107,7 +107,7 @@ fun SettingsFormatsPage(onBackPressed: () -> Unit) { PreferenceItem( title = stringResource(R.string.audio_provider), description = stringResource(R.string.audio_provider_desc), - icon = Icons.Outlined.HighQuality, + icon = Icons.Outlined.ShuffleOn, enabled = !isCustomCommandEnabled, ) { showAudioProviderDialog = true } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index aa3e9b1d..e304f013 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -180,24 +180,32 @@ object DownloaderUtil { //get the audio quality private fun SpotDLRequest.addAudioQuality(): SpotDLRequest = this.apply { when (PreferencesUtil.getAudioQuality()) { - 0 -> addOption("--bitrate", "8k") - 1 -> addOption("--bitrate", "16k") - 2 -> addOption("--bitrate", "24k") - 3 -> addOption("--bitrate", "32k") - 4 -> addOption("--bitrate", "40k") - 5 -> addOption("--bitrate", "48k") - 6 -> addOption("--bitrate", "64k") - 7 -> addOption("--bitrate", "80k") - 8 -> addOption("--bitrate", "96k") - 9 -> addOption("--bitrate", "112k") - 10 -> addOption("--bitrate", "128k") - 11 -> addOption("--bitrate", "160k") - 12 -> addOption("--bitrate", "192k") - 13 -> addOption("--bitrate", "224k") - 14 -> addOption("--bitrate", "256k") - 15 -> addOption("--bitrate", "320k") - 16 -> addOption("--bitrate", "disable") - 17 -> addOption("--bitrate", "auto") + 0 -> addOption("--bitrate", "auto") + 1 -> addOption("--bitrate", "8k") + 2 -> addOption("--bitrate", "16k") + 3 -> addOption("--bitrate", "24k") + 4 -> addOption("--bitrate", "32k") + 5 -> addOption("--bitrate", "40k") + 6 -> addOption("--bitrate", "48k") + 7 -> addOption("--bitrate", "64k") + 8 -> addOption("--bitrate", "80k") + 9 -> addOption("--bitrate", "96k") + 10 -> addOption("--bitrate", "112k") + 11 -> addOption("--bitrate", "128k") + 12 -> addOption("--bitrate", "160k") + 13 -> addOption("--bitrate", "192k") + 14 -> addOption("--bitrate", "224k") + 15 -> addOption("--bitrate", "256k") + 16 -> addOption("--bitrate", "320k") + 17 -> addOption("--bitrate", "disable") + } + } + + private fun SpotDLRequest.addAudioProvider(): SpotDLRequest = this.apply { + when (PreferencesUtil.getAudioProvider()) { + 0 -> null + 1 -> addOption("--provider", "youtube-music") + 2 -> addOption("--provider", "youtube") } } @@ -259,7 +267,7 @@ object DownloaderUtil { addAudioFormat() } - addOption("--audio", PreferencesUtil.getAudioProviderDesc()) + addAudioProvider() if (useSpotifyPreferences) { if (spotifyClientID.isEmpty() || spotifyClientSecret.isEmpty()) return Result.failure( diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt index d0717fdc..ff62244a 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.res.stringResource import androidx.core.os.LocaleListCompat import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.App.Companion.applicationScope +import com.bobbyesp.spowlo.App.Companion.context import com.bobbyesp.spowlo.App.Companion.isFDroidBuild import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.database.CommandTemplate @@ -161,16 +162,17 @@ object PreferencesUtil { 2 -> "ogg" 3 -> "opus" 4 -> "m4a" - 5 -> "Default" + 5 -> context.getString(R.string.not_specified) else -> "mp3" } } fun getAudioProviderDesc(audioProviderInt: Int = getAudioProvider()): String { return when (audioProviderInt){ - 0 -> "youtube-music" - 1 -> "youtube" - else -> "youtube-music" + 0 -> context.getString(R.string.default_option) + 1 -> "Youtube Music" + 2 -> "Youtube" + else -> "Youtube Music" } } @@ -185,24 +187,24 @@ object PreferencesUtil { fun getAudioQualityDesc(audioQualityStr: Int = getAudioQuality()): String { return when (audioQualityStr) { - 0 -> "8k" - 1 -> "16k" - 2 -> "24k" - 3 -> "32k" - 4 -> "40k" - 5 -> "48k" - 6 -> "64k" - 7 -> "80k" - 8 -> "96k" - 9 -> "112k" - 10 -> "128k" - 11 -> "160k" - 12 -> "192k" - 13 -> "224k" - 14 -> "256k" - 15 -> "320k" - 16 -> "disable" - 17 -> "auto" + 0 -> context.getString(R.string.not_specified) + 1 -> "8k" + 2 -> "16k" + 3 -> "24k" + 4 -> "32k" + 5 -> "40k" + 6 -> "48k" + 7 -> "64k" + 8 -> "80k" + 9 -> "96k" + 10 -> "112k" + 11 -> "128k" + 12 -> "160k" + 13 -> "192k" + 14 -> "224k" + 15 -> "256k" + 16 -> "320k" + 17 -> context.getString(R.string.not_convert) else -> "auto" } } @@ -308,12 +310,6 @@ object PreferencesUtil { applicationScope, started = SharingStarted.Eagerly, emptyList() ) - fun getTemplate(): CommandTemplate { - return templateStateFlow.value.run { - find { it.id == TEMPLATE_ID.getInt() } ?: first() - } - } - } data class DarkThemePreference( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 630b2b1c..3a8d0ba6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -263,4 +263,11 @@ No results were found Single Loading the page... + Not specified + Default + Don\'t convert + An error ocurried while searching + Type something on the text box for searching through Spotify! + Realod the page + Copy the error \ No newline at end of file From 795772b61920e418793c6fdaf104991e06c55f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= <60316747+BobbyESP@users.noreply.github.com> Date: Tue, 14 Mar 2023 19:14:48 +0100 Subject: [PATCH 11/42] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index fb285631..4b6cf145 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,7 @@ A Spotify songs downloader powered by [spotDL](https://github.com/spotDL/spotify ## ⚠️ Warning -The Spotify mods downloader has been deleted by request of the xManager team. This is because having the mod downloader in Spowlo meant an avoid of their ads/earning methods. xManager has to pay servers and they pay those just for making the users have free Spotify, I hope that you all understand. - -please, instead use the [xManager app](https://github.com/xManager-App/xManager). Maybe somme day I create an app for them who knows haha +Spowlo uses YT Music and YouTube to download the songs. This is because Spotify DRM bypassing can lead to an account ban and legal issues. If YT Music isn't available in your country, don't worry, you can still use YouTube as audio provider or use a VPN. We are working on making a regional bypass so don't matter your region. Thank you for understanding. ## 🔮 Features From b098995ac009a420d924918a29d386bccd1a6ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Wed, 15 Mar 2023 22:03:02 +0100 Subject: [PATCH 12/42] feat: changed metadata viewer (started to implement) --- .../java/com/bobbyesp/spowlo/Downloader.kt | 48 ++++++---- .../spotify_api/SpotifyApiRequests.kt | 13 +++ .../data/binders/SpotifyItemBinder.kt | 2 - .../spotify_api/data/dtos/SpotifyData.kt | 3 + .../history/HistoryMediaComponents.kt | 14 +-- .../ui/dialogs/DownloaderSettingsDialog.kt | 44 +++++---- .../pages/downloader/DownloaderViewModel.kt | 7 +- .../ui/pages/history/DownloadsHistoryPage.kt | 4 +- .../binders/SpotifyInfoBinder.kt | 17 ++++ .../binders/SpotifyPageBinder.kt | 37 ++++++++ .../pages/metadata_viewer/pages/AlbumPage.kt | 13 +++ .../pages/metadata_viewer/pages/ArtistPage.kt | 12 +++ .../metadata_viewer/pages/PlaylistViewPage.kt | 12 +++ .../pages/metadata_viewer/pages/TrackPage.kt | 92 +++++++++++++++++++ .../metadata_viewer/playlists/PlaylistPage.kt | 45 +++++---- .../playlists/PlaylistPageViewModel.kt | 32 ++++++- .../spowlo/ui/pages/searcher/SearcherPage.kt | 14 +-- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 4 +- .../bobbyesp/spowlo/utils/PreferencesUtil.kt | 3 + app/src/main/res/values/strings.xml | 5 + 20 files changed, 338 insertions(+), 83 deletions(-) delete mode 100644 app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/binders/SpotifyItemBinder.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index 0f825145..4b70bb33 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -104,7 +104,7 @@ object Downloader { } @CheckResult - private suspend fun downloadSong( + private fun downloadSong( songInfo: Song, preferences: DownloaderUtil.DownloadPreferences = DownloaderUtil.DownloadPreferences() ): Result> { @@ -162,29 +162,44 @@ object Downloader { fun getInfoAndDownload( url: String, - downloadPreferences: DownloaderUtil.DownloadPreferences = DownloaderUtil.DownloadPreferences() + downloadPreferences: DownloaderUtil.DownloadPreferences = DownloaderUtil.DownloadPreferences(), + skipInfoFetch: Boolean = false ) { currentJob = applicationScope.launch(Dispatchers.IO) { updateState(State.FetchingInfo) - DownloaderUtil.fetchSongInfoFromUrl( - url = url, - preferences = downloadPreferences - ) - .onFailure { + if(skipInfoFetch){ + downloadResultTemp = downloadSong( + songInfo = Song(url = url), + preferences = downloadPreferences + ).onFailure { manageDownloadError( it, isFetchingInfo = true, isTaskAborted = true ) } - .onSuccess { info -> - for (song in info) { - downloadResultTemp = downloadSong( - songInfo = song, - preferences = downloadPreferences + return@launch + } + else { + DownloaderUtil.fetchSongInfoFromUrl( + url = url + ) + .onFailure { + manageDownloadError( + it, + isFetchingInfo = true, + isTaskAborted = true ) } - } + .onSuccess { info -> + for (song in info) { + downloadResultTemp = downloadSong( + songInfo = song, + preferences = downloadPreferences + ) + } + } + } } } @@ -195,8 +210,7 @@ object Downloader { currentJob = applicationScope.launch(Dispatchers.IO) { updateState(State.FetchingInfo) DownloaderUtil.fetchSongInfoFromUrl( - url = url, - preferences = downloadPreferences + url = url ) .onFailure { manageDownloadError( @@ -213,7 +227,7 @@ object Downloader { } } - fun updateState(state: State) = mutableDownloaderState.update { state } + private fun updateState(state: State) = mutableDownloaderState.update { state } fun clearErrorState() { mutableErrorState.update { ErrorState() } @@ -248,7 +262,7 @@ object Downloader { /** * @param isTaskAborted Determines if the download task is aborted due to the given `Exception` */ - fun manageDownloadError( + private fun manageDownloadError( th: Throwable, isFetchingInfo: Boolean, isTaskAborted: Boolean = true, diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt index 913081ea..e289e54d 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt @@ -6,6 +6,7 @@ import com.adamratzman.spotify.models.Playlist import com.adamratzman.spotify.models.SpotifyPublicUser import com.adamratzman.spotify.models.SpotifySearchResult import com.adamratzman.spotify.models.Token +import com.adamratzman.spotify.models.Track import com.adamratzman.spotify.spotifyAppApi import com.adamratzman.spotify.utils.Market import com.bobbyesp.spowlo.BuildConfig @@ -76,4 +77,16 @@ object SpotifyApiRequests { } return null } + + suspend fun searchTrackById(id: String): Track? { + kotlin.runCatching { + api!!.tracks.getTrack(id, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return null + }.onSuccess { + return it + } + return null + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/binders/SpotifyItemBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/binders/SpotifyItemBinder.kt deleted file mode 100644 index e6568a56..00000000 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/binders/SpotifyItemBinder.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.bobbyesp.spowlo.features.spotify_api.data.binders - diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt index 12eafaba..95db5904 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt @@ -1,9 +1,12 @@ package com.bobbyesp.spowlo.features.spotify_api.data.dtos +import com.adamratzman.spotify.models.ReleaseDate + data class SpotifyData( val artworkUrl: String = "", val name: String = "", val artists: List = emptyList(), + val releaseDate: ReleaseDate? = null, val type: SpotifyDataType = SpotifyDataType.TRACK ) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/history/HistoryMediaComponents.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/history/HistoryMediaComponents.kt index 8580104d..a873cfbf 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/history/HistoryMediaComponents.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/history/HistoryMediaComponents.kt @@ -3,7 +3,6 @@ package com.bobbyesp.spowlo.ui.components.history import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -16,18 +15,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.selectable import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProgressIndicatorDefaults -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -45,10 +39,6 @@ import com.bobbyesp.spowlo.ui.common.AsyncImageImpl import com.bobbyesp.spowlo.ui.common.LocalWindowWidthState import com.bobbyesp.spowlo.ui.components.MarqueeText import com.bobbyesp.spowlo.ui.components.songs.CustomTag -import com.bobbyesp.spowlo.ui.components.songs.ExplicitIcon -import com.bobbyesp.spowlo.ui.components.songs.LyricsIcon -import com.bobbyesp.spowlo.ui.components.songs.MiniMetadataInfoComponent -import com.bobbyesp.spowlo.utils.GeneralTextUtils import com.bobbyesp.spowlo.utils.toFileSizeText @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @@ -146,7 +136,7 @@ fun HistoryMediaItem( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start ) { - if (!isTwoColumns) { + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center @@ -185,7 +175,7 @@ fun HistoryMediaItem( maxLines = 1, ) } - } + } Column( modifier = Modifier diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt index 3a6a67de..9b984a84 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -66,6 +67,7 @@ import com.bobbyesp.spowlo.utils.GEO_BYPASS import com.bobbyesp.spowlo.utils.ORIGINAL_AUDIO import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.PreferencesUtil.templateStateFlow +import com.bobbyesp.spowlo.utils.SKIP_INFO_FETCH import com.bobbyesp.spowlo.utils.SYNCED_LYRICS import com.bobbyesp.spowlo.utils.TEMPLATE_ID import com.bobbyesp.spowlo.utils.USE_CACHING @@ -151,6 +153,8 @@ fun DownloaderSettingsDialog( ) } + var skipInfoFetch by remember { mutableStateOf(settings.getValue(SKIP_INFO_FETCH)) } + var showAudioFormatDialog by remember { mutableStateOf(false) } var showAudioQualityDialog by remember { mutableStateOf(false) } var showClientIdDialog by remember { mutableStateOf(false) } @@ -190,10 +194,11 @@ fun DownloaderSettingsDialog( Column { Text( text = stringResource(R.string.settings_before_download_text), - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.CenterHorizontally) + modifier = Modifier.align(Alignment.Start) ) AnimatedVisibility(visible = preserveOriginalAudio) { ElevatedCard( @@ -373,20 +378,27 @@ fun DownloaderSettingsDialog( if (!useDialog) { //TODO: Change this UI BottomDrawer(drawerState = drawerState, sheetContent = { - Icon( - modifier = Modifier.align(Alignment.CenterHorizontally), - imageVector = Icons.Outlined.DownloadDone, - contentDescription = null - ) - Text( - text = stringResource(R.string.settings_before_download), - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(vertical = 16.dp), - maxLines = 2, - overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.DownloadDone, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp) + ) + Text( + text = stringResource(R.string.settings_before_download), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier + .padding(vertical = 16.dp), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold + ) + } sheetContent() val state = rememberLazyListState() LaunchedEffect(drawerState.isVisible) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderViewModel.kt index cec1725e..747c3f70 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderViewModel.kt @@ -8,7 +8,6 @@ import com.bobbyesp.library.dto.Song import com.bobbyesp.spowlo.Downloader import com.bobbyesp.spowlo.Downloader.showErrorMessage import com.bobbyesp.spowlo.R -import com.bobbyesp.spowlo.utils.DownloaderUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -24,7 +23,7 @@ class DownloaderViewModel @Inject constructor() : ViewModel() { private val mutableViewStateFlow = MutableStateFlow(ViewState()) val viewStateFlow = mutableViewStateFlow.asStateFlow() - val songInfoFlow = MutableStateFlow(listOf(Song())) + private val songInfoFlow = MutableStateFlow(listOf(Song())) data class ViewState( val url: String = "", @@ -68,7 +67,7 @@ class DownloaderViewModel @Inject constructor() : ViewModel() { Downloader.getRequestedMetadata(url) } - fun startDownloadSong() { + fun startDownloadSong(skipInfoFetch: Boolean = false) { val url = viewStateFlow.value.url Downloader.clearErrorState() if (!Downloader.isDownloaderAvailable()) @@ -77,7 +76,7 @@ class DownloaderViewModel @Inject constructor() : ViewModel() { showErrorMessage(R.string.url_empty) return } - Downloader.getInfoAndDownload(url) + Downloader.getInfoAndDownload(url, skipInfoFetch = skipInfoFetch) } fun goToMetadataViewer(songs: List) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadsHistoryPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadsHistoryPage.kt index aa03eb72..6dacaff1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadsHistoryPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadsHistoryPage.kt @@ -300,7 +300,9 @@ fun DownloadsHistoryPage( if (songsList.isEmpty()) { item { - Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { + Column(modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { EmptyState( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt new file mode 100644 index 00000000..d10f4396 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt @@ -0,0 +1,17 @@ +package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyDataType + +//make a composable that has as parameter a SpotifyData and returns the type of the data as a string with a string resource +@Composable +fun typeOfDataToString(type: SpotifyDataType): String { + return when (type) { + SpotifyDataType.ALBUM -> stringResource(id = R.string.album) + SpotifyDataType.ARTIST -> stringResource(id = R.string.artist) + SpotifyDataType.PLAYLIST -> stringResource(id = R.string.playlist) + SpotifyDataType.TRACK -> stringResource(id = R.string.track) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt new file mode 100644 index 00000000..b7ccf0bd --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt @@ -0,0 +1,37 @@ +package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyDataType +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.AlbumPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.ArtistPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.PlaylistViewPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.TrackPage + +@Composable +fun SpotifyPageBinder( + spotifyData: SpotifyData, + modifier : Modifier = Modifier +) { + Column(modifier = modifier) { + when (spotifyData.type) { + SpotifyDataType.ALBUM -> { + AlbumPage(spotifyData, modifier) + } + + SpotifyDataType.ARTIST -> { + ArtistPage(spotifyData, modifier) + } + + SpotifyDataType.PLAYLIST -> { + PlaylistViewPage(spotifyData, modifier) + } + + SpotifyDataType.TRACK -> { + TrackPage(spotifyData, modifier) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt new file mode 100644 index 00000000..66774030 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt @@ -0,0 +1,13 @@ +package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData + +@Composable +fun AlbumPage( + data: SpotifyData, + modifier: Modifier +) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt new file mode 100644 index 00000000..9b3dec54 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt @@ -0,0 +1,12 @@ +package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData + +@Composable +fun ArtistPage( + data: SpotifyData, + modifier: Modifier +) { +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt new file mode 100644 index 00000000..ed18507f --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -0,0 +1,12 @@ +package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData + +@Composable +fun PlaylistViewPage( + data: SpotifyData, + modifier: Modifier +) { +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt new file mode 100644 index 00000000..ebbb85a4 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -0,0 +1,92 @@ +package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.selection.SelectionContainer +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.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.ui.common.AsyncImageImpl +import com.bobbyesp.spowlo.ui.components.MarqueeText +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfDataToString + +@Composable +fun TrackPage( + data: SpotifyData, + modifier: Modifier +) { + val localConfig = LocalConfiguration.current + Column(modifier = modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp)) { + Box( + modifier = Modifier + .clip(MaterialTheme.shapes.extraSmall) + .fillMaxWidth() + .padding(top = 6.dp, bottom = 6.dp), + contentAlignment = Alignment.Center + ) { + //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) + val size = (localConfig.screenWidthDp / 2) * 1.5 + AsyncImageImpl( + modifier = Modifier + .size(size.dp) + .aspectRatio( + 1f, + matchHeightConstraintsFirst = true + ) + .clip(MaterialTheme.shapes.small), + model = data.artworkUrl, + contentDescription = stringResource(id = R.string.track_artwork), + contentScale = ContentScale.Crop, + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + ) { + SelectionContainer { + MarqueeText( + text = data.name, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineMedium + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = data.artists.joinToString(", ") { it }, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = typeOfDataToString(data.type) + " • " + data.releaseDate?.year.toString(), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt index 60ce06fe..5be67e44 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt @@ -1,9 +1,11 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -13,16 +15,17 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.unit.sp +import androidx.compose.ui.text.style.TextOverflow import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.pages.common.LoadingPage -import kotlinx.coroutines.delay +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.SpotifyPageBinder -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun PlaylistPage( onBackPressed: () -> Unit, @@ -34,45 +37,53 @@ fun PlaylistPage( val viewState by playlistPageViewModel.viewStateFlow.collectAsStateWithLifecycle() - LaunchedEffect(Unit){ - delay(1000) - playlistPageViewModel.loadData() + LaunchedEffect(Unit) { + playlistPageViewModel.loadData(id) } with(viewState) { when (this.state) { - is PlaylistDataState.Loading -> { + is PlaylistDataState.Loading -> { LoadingPage() } - is PlaylistDataState.Error -> { + is PlaylistDataState.Error -> { Text(text = this.state.error.message.toString()) } - is PlaylistDataState.Loaded -> { + is PlaylistDataState.Loaded -> { Scaffold(modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { TopAppBar(title = { Text( - text = "Playlist (WIP)", - style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp) + "WORK IN PROGRESS", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.alpha(scrollBehavior.state.overlappedFraction) ) }, navigationIcon = { BackButton { onBackPressed() } }, actions = { - }, scrollBehavior = scrollBehavior + }, + scrollBehavior = scrollBehavior ) }) { paddings -> - Text(text = this.state.data.toString() + id, modifier = Modifier.padding(paddings)) + LazyColumn( + Modifier + .padding(paddings) + .fillMaxSize()) { + item{ + Box(Modifier.animateItemPlacement()) { + SpotifyPageBinder(spotifyData = state.data, modifier = Modifier) + } + } + } } - } - } } - } sealed class PlaylistDataState { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt index f9c334ae..7a265014 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt @@ -1,6 +1,7 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists import androidx.lifecycle.ViewModel +import com.bobbyesp.spowlo.features.spotify_api.SpotifyApiRequests import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyDataType import kotlinx.coroutines.flow.MutableStateFlow @@ -14,13 +15,34 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { val viewStateFlow = mutableViewStateFlow.asStateFlow() data class ViewState( - val id : String = "", - val state : PlaylistDataState = PlaylistDataState.Loading, + val id: String = "", + val state: PlaylistDataState = PlaylistDataState.Loading, ) - suspend fun loadData(){ - mutableViewStateFlow.update { - it.copy(state = PlaylistDataState.Loaded(SpotifyData("", "Faded", listOf("Alan Walker"), SpotifyDataType.TRACK))) + suspend fun loadData(id: String) { + kotlin.runCatching { + SpotifyApiRequests.searchTrackById(id) + }.onSuccess { track -> + mutableViewStateFlow.update { + it.copy( + state = PlaylistDataState.Loaded( + SpotifyData( + track!!.album.images[0].url, + track.name, + track.artists.map { it.name }, + track.album.releaseDate, + SpotifyDataType.TRACK + ) + ) + ) + } + }.onFailure { + mutableViewStateFlow.update { + it.copy( + state = PlaylistDataState.Error(Exception("Error while loading data")) + ) + } } + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 5731b913..2a3f2f4c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -39,6 +39,8 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -117,7 +119,7 @@ fun SearcherPageImpl( modifier = Modifier.align( Alignment.CenterHorizontally ), - style = MaterialTheme.typography.displaySmall, + style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold ) } @@ -171,11 +173,11 @@ fun SearcherPageImpl( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = stringResource(id = R.string.error), - modifier = Modifier.align( - Alignment.CenterHorizontally - ), - style = MaterialTheme.typography.displaySmall, + text = stringResource(R.string.error), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, fontWeight = FontWeight.Bold ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index e304f013..944768e3 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -141,7 +141,6 @@ object DownloaderUtil { @CheckResult private fun getSongInfo( url: String? = null, - id: String = getRandomUUID() ): Result> = kotlin.runCatching { val response: List = SpotDL.getInstance().getSongInfo(url ?: "") @@ -153,7 +152,7 @@ object DownloaderUtil { @CheckResult fun fetchSongInfoFromUrl( - url: String, playlistItem: Int = 0, preferences: DownloadPreferences = DownloadPreferences() + url: String ): Result> = kotlin.run { getSongInfo(url) @@ -222,7 +221,6 @@ object DownloaderUtil { with(downloadPreferences) { val url = playlistUrl.ifEmpty { songInfo.url - ?: return Result.failure(Throwable(context.getString(R.string.fetch_info_error_msg))) } val request = SpotDLRequest() val pathBuilder = StringBuilder() diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt index ff62244a..a10db320 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt @@ -54,6 +54,8 @@ const val USE_YT_METADATA = "use_yt_metadata" const val USE_SPOTIFY_CREDENTIALS = "use_spotify_credentials" const val SYNCED_LYRICS = "synced_lyrics" +const val SKIP_INFO_FETCH = "skip_info_fetch" + const val SPOTIFY_CLIENT_ID = "spotify_client_id" const val SPOTIFY_CLIENT_SECRET = "spotify_client_secret" @@ -103,6 +105,7 @@ private val BooleanPreferenceDefaults = DONT_FILTER_RESULTS to false, SPOTDL_UPDATE to true, GEO_BYPASS to false, + SKIP_INFO_FETCH to false, ) private val IntPreferenceDefaults = mapOf( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a8d0ba6..2feafb31 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -270,4 +270,9 @@ Type something on the text box for searching through Spotify! Realod the page Copy the error + Track artwork + Album + Artist + Playlist + Track \ No newline at end of file From f50233018a666a0a5be2bea3f105421b259380e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Thu, 16 Mar 2023 17:28:46 +0100 Subject: [PATCH 13/42] chore: Added NotImplementedPage, added playlists to the search and modularized data items in the search page --- .../spotify_api/SpotifyApiRequests.kt | 12 -- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 2 +- .../ui/pages/common/NotImplementedPage.kt | 38 +++++ .../binders/SpotifyInfoBinder.kt | 13 ++ .../pages/metadata_viewer/pages/AlbumPage.kt | 3 +- .../pages/metadata_viewer/pages/ArtistPage.kt | 2 + .../metadata_viewer/pages/PlaylistViewPage.kt | 2 + .../pages/metadata_viewer/pages/TrackPage.kt | 6 +- .../playlists/PlaylistPageViewModel.kt | 5 +- .../spowlo/ui/pages/searcher/SearcherPage.kt | 148 +++++++++++++++--- app/src/main/res/values/strings.xml | 3 + 11 files changed, 195 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/NotImplementedPage.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt index e289e54d..8cf5d9fe 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt @@ -41,18 +41,6 @@ object SpotifyApiRequests { } // Performs Spotify database query for queries related to track information. - suspend fun trackSearch(searchQuery: String): SpotifySearchResult { - kotlin.runCatching { - api!!.search.searchAllTypes(searchQuery, limit = 50, offset = 1, market = Market.ES) - }.onFailure { - Log.d("SpotifyApiRequests", "Error: ${it.message}") - return SpotifySearchResult() - }.onSuccess { - return it - } - return SpotifySearchResult() - } - suspend fun searchAllTypes(searchQuery: String): SpotifySearchResult { kotlin.runCatching { api!!.search.searchAllTypes(searchQuery, limit = 50, offset = 1, market = Market.ES) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index df8d7057..504e2adb 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -465,7 +465,7 @@ fun InitialEntry( it.printStackTrace() ToastUtil.makeToastSuspend(context.getString(R.string.spotify_api_error)) }.onSuccess { - val req = SpotifyApiRequests.trackSearch("Faded Alan Walker") + val req = SpotifyApiRequests.searchAllTypes("Faded Alan Walker") Log.d("InitialEntry", "Name:" + req.tracks!![0].name) Log.d("InitialEntry", "Artist:" + req.tracks!![0].artists[0].name) Log.d("InitialEntry", "Album:" + req.tracks!![0].album.name) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/NotImplementedPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/NotImplementedPage.kt new file mode 100644 index 00000000..1b77752c --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/NotImplementedPage.kt @@ -0,0 +1,38 @@ +package com.bobbyesp.spowlo.ui.pages.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +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.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import com.bobbyesp.spowlo.R + +@Composable +fun NotImplementedPage( +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.not_implemented), modifier = Modifier.align( + Alignment.CenterHorizontally + ), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold + ) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt index d10f4396..3ebfe43a 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt @@ -1,5 +1,6 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.bobbyesp.spowlo.R @@ -14,4 +15,16 @@ fun typeOfDataToString(type: SpotifyDataType): String { SpotifyDataType.PLAYLIST -> stringResource(id = R.string.playlist) SpotifyDataType.TRACK -> stringResource(id = R.string.track) } +} + +//Assign and return the type of the data from referred to the SpotifyDataType enum +fun typeOfSpotifyDataType(type: String): SpotifyDataType { + Log.d("SpotifyDataType", "Type: $type") + return when (type) { + "track" -> SpotifyDataType.TRACK + "album" -> SpotifyDataType.ALBUM + "playlist" -> SpotifyDataType.PLAYLIST + "artist" -> SpotifyDataType.ARTIST + else -> SpotifyDataType.TRACK + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt index 66774030..5298847d 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt @@ -3,11 +3,12 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage @Composable fun AlbumPage( data: SpotifyData, modifier: Modifier ) { - + NotImplementedPage() } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt index 9b3dec54..cf84664a 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt @@ -3,10 +3,12 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage @Composable fun ArtistPage( data: SpotifyData, modifier: Modifier ) { + NotImplementedPage() } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index ed18507f..7f6ffa9e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -3,10 +3,12 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage @Composable fun PlaylistViewPage( data: SpotifyData, modifier: Modifier ) { + NotImplementedPage() } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index ebbb85a4..68747dc8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -35,7 +35,7 @@ fun TrackPage( val localConfig = LocalConfiguration.current Column(modifier = modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp)) { + .padding(start = 16.dp, end = 16.dp, top = 12.dp)) { Box( modifier = Modifier .clip(MaterialTheme.shapes.extraSmall) @@ -43,8 +43,8 @@ fun TrackPage( .padding(top = 6.dp, bottom = 6.dp), contentAlignment = Alignment.Center ) { - //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) - val size = (localConfig.screenWidthDp / 2) * 1.5 + //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height + val size = (localConfig.screenHeightDp / 3) AsyncImageImpl( modifier = Modifier .size(size.dp) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt index 7a265014..237ddabd 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import com.bobbyesp.spowlo.features.spotify_api.SpotifyApiRequests import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyDataType +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -19,7 +20,7 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { val state: PlaylistDataState = PlaylistDataState.Loading, ) - suspend fun loadData(id: String) { + suspend fun loadData(id: String, type: SpotifyDataType = SpotifyDataType.TRACK) { kotlin.runCatching { SpotifyApiRequests.searchTrackById(id) }.onSuccess { track -> @@ -31,7 +32,7 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { track.name, track.artists.map { it.name }, track.album.releaseDate, - SpotifyDataType.TRACK + typeOfSpotifyDataType(track.type), ) ) ) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 2a3f2f4c..1739ac3b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -49,6 +49,8 @@ import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.songs.search_feat.SearchingSongComponent +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfDataToString +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType import com.bobbyesp.spowlo.ui.theme.harmonizeWithPrimary import kotlinx.coroutines.delay @@ -90,7 +92,11 @@ fun SearcherPageImpl( with(viewState) { Column(modifier = Modifier.fillMaxSize()) { QueryTextBox( - modifier = Modifier.padding(), + modifier = Modifier.padding( + top = 16.dp, + start = 16.dp, + end = 16.dp + ), query = query, onValueChange = { query -> onValueChange(query) @@ -186,25 +192,109 @@ fun SearcherPageImpl( } is ViewSearchState.Success -> { - if (viewState.viewState.data.tracks != null) { - items(viewState.viewState.data.tracks!!.size) { track -> - with(viewState.viewState.data.tracks!![track]) { - val artists: List = - this.artists.map { artist -> artist.name } - SearchingSongComponent( - artworkUrl = album.images[0].url, - songName = this.name, - artists = artists.joinToString(", "), - spotifyUrl = this.externalUrls.spotify ?: "", - onClick = { onItemClick(this.id) } + val allItems = + mutableListOf() //TODO: Add the filters. Pagination should be done in the future + viewState.viewState.data.let { data -> + data.albums?.items?.let { allItems.addAll(it) } + data.artists?.items?.let { allItems.addAll(it) } + data.playlists?.items?.let { allItems.addAll(it) } + data.tracks?.items?.let { allItems.addAll(it) } + data.episodes?.items?.let { + allItems.addAll( + listOf( + it ) - } - //if it is not the last item, add a horizontal divider - if (track != viewState.viewState.data.tracks!!.size - 1) { - HorizontalDivider( - modifier = Modifier.alpha(0.35f), - color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) + } + data.shows?.items?.let { + allItems.addAll( + listOf( + it + ) + ) + } + if (data != null) { //You may think that this is not necessary, but it is + item { + Text( + text = stringResource(R.string.showing_results).format( + allItems.size + ), + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier + .padding(16.dp) + .alpha(0.7f), + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Start, + fontWeight = FontWeight.Bold ) + + } + data.albums?.items?.forEachIndexed { index, album -> + item { + // TODO: Display the album item + } + } + data.artists?.items?.forEachIndexed { index, artist -> + item { + // TODO: Display the artist item + } + } + + data.tracks?.items?.forEachIndexed { index, track -> + item { + val artists: List = + track.artists.map { artist -> artist.name } + SearchingSongComponent( + artworkUrl = track.album.images[2].url, + songName = track.name, + artists = artists.joinToString(", "), + spotifyUrl = track.externalUrls.spotify ?: "", + onClick = { onItemClick(track.id) }, + type = typeOfDataToString( + type = typeOfSpotifyDataType( + track.type + ) + ) + ) + HorizontalDivider( + modifier = Modifier.alpha(0.35f), + color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) + } + } + + data.playlists?.items?.forEachIndexed { index, playlist -> + item { + SearchingSongComponent( + artworkUrl = playlist.images[0].url, + songName = playlist.name, + artists = playlist.owner.displayName + ?: stringResource(R.string.unknown), + spotifyUrl = playlist.externalUrls.spotify ?: "", + onClick = { onItemClick(playlist.id) }, + type = typeOfDataToString( + type = typeOfSpotifyDataType( + playlist.type + ) + ) + ) + HorizontalDivider( + modifier = Modifier.alpha(0.35f), + color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) + } + } + data.episodes?.items?.forEachIndexed { index, episode -> + item { + // TODO: Display the episode item + } + } + data.shows?.items?.forEachIndexed { index, show -> + item { + // TODO: Display the show item + } } } } @@ -236,7 +326,6 @@ fun QueryTextBox( } }, modifier = modifier - .padding(top = 16.dp, bottom = 4.dp, start = 16.dp, end = 16.dp) .fillMaxWidth() .focusRequester(focusRequester), keyboardOptions = KeyboardOptions( @@ -265,4 +354,23 @@ fun QueryTextBox( ), unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant ), ) -} \ No newline at end of file +} + +enum class FilterType { + ALL, ALBUMS, ARTISTS, PLAYLISTS, TRACKS, EPISODES, SHOWS +} +//TODO: Add filters +/* +* val filterState = rememberSaveable { mutableStateOf(FilterType.ALL) } + + // Filter the items based on the selected filter type + val filteredItems = when (filterState.value) { + FilterType.ALL -> allItems + FilterType.ALBUMS -> allItems.filterIsInstance() + FilterType.ARTISTS -> allItems.filterIsInstance() + FilterType.PLAYLISTS -> allItems.filterIsInstance() + FilterType.TRACKS -> allItems.filterIsInstance() + FilterType.EPISODES -> allItems.filterIsInstance() + FilterType.SHOWS -> allItems.filterIsInstance() + } +* */ \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2feafb31..30fe3ce4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -275,4 +275,7 @@ Artist Playlist Track + Shwowing %1$d results + Sorry, this page is not yet implemented ;( + Go back \ No newline at end of file From bdeadf431aa9ff63395e590b6983313666d43fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Thu, 16 Mar 2023 23:19:49 +0100 Subject: [PATCH 14/42] chore: added new buttons for the settings page (not yet implemented) --- .../components/settings/SettingsComponents.kt | 111 ++++++++++++++++++ .../spowlo/ui/pages/settings/SettingsPage.kt | 1 + .../settings/spotify/SpotifySettingsPage.kt | 94 ++++++++++----- 3 files changed, 174 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt new file mode 100644 index 00000000..802e1ccf --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt @@ -0,0 +1,111 @@ +package com.bobbyesp.spowlo.ui.components.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun SettingsItemNew( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + description: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, + icon: ImageVector? = null, + addTonalElevation: Boolean = false, + clipCorners: Boolean = false +) { + ListItem( + modifier = Modifier + .apply { if (clipCorners) this.clip(MaterialTheme.shapes.medium) } + .then(modifier), + leadingContent = { + icon?.let { + Icon( + imageVector = icon, + contentDescription = null, + ) + } + }, + trailingContent = trailing, + supportingContent = description, + headlineContent = title, + tonalElevation = if (addTonalElevation) 3.dp else 0.dp + ) +} + +@Composable +fun SettingsItemNew( + onClick: () -> Unit, + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + description: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, + icon: ImageVector? = null, + addTonalElevation: Boolean = false, + clipCorners: Boolean = false +) { + SettingsItemNew( + modifier = modifier + .clickable( + onClick = onClick, + enabled = enabled + ) + .alpha(if (enabled) 1f else 0.5f), + icon = icon, + description = description, + title = title, + trailing = trailing, + addTonalElevation = addTonalElevation, + clipCorners = clipCorners + ) +} + +@Composable +fun SettingsSwitch( + onCheckedChange: ((Boolean) -> Unit)?, + checked: Boolean, + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + description: (@Composable () -> Unit)? = null, + icon: ImageVector? = null, + thumbContent: (@Composable () -> Unit)? = null, + addTonalElevation: Boolean = false, + clipCorners: Boolean = false +) { + val toggleableModifier = if (onCheckedChange != null) { + Modifier.toggleable( + value = checked, + enabled = enabled, + onValueChange = onCheckedChange + ).apply { if (!enabled) this.alpha(0.5f) } + } else Modifier + + SettingsItemNew( + modifier = modifier + .then(toggleableModifier), + icon = icon, + description = description, + title = title, + trailing = { + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + thumbContent = thumbContent + ) + }, + addTonalElevation = addTonalElevation, + clipCorners = clipCorners + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt index a01400a7..4819ed3c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt @@ -81,6 +81,7 @@ fun SettingsPage(navController: NavController) { scrollBehavior = scrollBehavior ) }) { + LazyColumn( modifier = Modifier.padding(it) ) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt index 639ba1ef..4b3b35a8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt @@ -6,15 +6,20 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.PermIdentity +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -25,9 +30,9 @@ import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.LargeTopAppBar import com.bobbyesp.spowlo.ui.components.PreferenceInfo -import com.bobbyesp.spowlo.ui.components.PreferenceItem import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle -import com.bobbyesp.spowlo.ui.components.PreferenceSwitch +import com.bobbyesp.spowlo.ui.components.settings.SettingsItemNew +import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitch import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.SPOTIFY_CLIENT_ID import com.bobbyesp.spowlo.utils.SPOTIFY_CLIENT_SECRET @@ -82,39 +87,65 @@ fun SpotifySettingsPage(onBackPressed: () -> Unit) { }, scrollBehavior = scrollBehavior ) }, content = { - LazyColumn(Modifier.padding(it)) { + LazyColumn( + Modifier + .padding(it) + .padding(horizontal = 20.dp) + ) { item { PreferenceSubtitle(text = stringResource(id = R.string.general_settings)) } item { - PreferenceSwitch( - title = stringResource(id = R.string.use_spotify_credentials), - isChecked = useSpotifyCredentials, - onClick = { - useSpotifyCredentials = !useSpotifyCredentials - PreferencesUtil.updateValue(USE_SPOTIFY_CREDENTIALS, useSpotifyCredentials) - } - ) - PreferenceItem( - title = stringResource(id = R.string.spotify_client_id), - description = stringResource(id = R.string.spotify_client_id_description), - icon = Icons.Outlined.PermIdentity, - enabled = useSpotifyCredentials, - onClick = { - showClientIdDialog = true - } - ) - PreferenceItem( - title = stringResource(id = R.string.spotify_client_secret), - description = stringResource(id = R.string.spotify_client_secret_description), - icon = Icons.Outlined.Key, - enabled = useSpotifyCredentials, - onClick = { - showClientSecretDialog = true - } - ) + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) + ) { + SettingsSwitch( + title = { + Text(stringResource(id = R.string.use_spotify_credentials)) + }, + checked = useSpotifyCredentials, + onCheckedChange = { + useSpotifyCredentials = !useSpotifyCredentials + PreferencesUtil.updateValue( + USE_SPOTIFY_CREDENTIALS, + useSpotifyCredentials + ) + }, + addTonalElevation = true + ) + Divider(color = MaterialTheme.colorScheme.surfaceVariant) + SettingsItemNew( + title = { + Text(stringResource(id = R.string.spotify_client_id)) + }, + description = { + Text(stringResource(id = R.string.spotify_client_id_description)) + }, + icon = Icons.Outlined.PermIdentity, + onClick = { + showClientIdDialog = true + }, + enabled = useSpotifyCredentials, + addTonalElevation = true + ) + + SettingsItemNew( + title = { + Text(stringResource(id = R.string.spotify_client_secret)) + }, + description = { + Text(stringResource(id = R.string.spotify_client_secret_description)) + }, + icon = Icons.Outlined.Key, + onClick = { + showClientSecretDialog = true + }, + enabled = useSpotifyCredentials, + addTonalElevation = true + ) + } } - item{ + item { HorizontalDivider(Modifier.padding(vertical = 6.dp)) PreferenceInfo( modifier = Modifier @@ -122,7 +153,6 @@ fun SpotifySettingsPage(onBackPressed: () -> Unit) { text = stringResource(id = R.string.spotify_credentials_info) ) } - } }) if (showClientIdDialog) { From 465e026db816e3c6d7fa02d05b166e1acb3d24c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 18 Mar 2023 19:45:46 +0000 Subject: [PATCH 15/42] Translated using Weblate (Spanish) Currently translated at 13.3% (37 of 277 strings) Translation: Spowlo/strings Translate-URL: https://hosted.weblate.org/projects/spowlo/strings/es/ --- app/src/main/res/values-es/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a6b3daec..1dd86092 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,2 +1,4 @@ - \ No newline at end of file + + Artista del álbum + \ No newline at end of file From 16b844e7e0147e582708e9383b9d0530a5293202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 18 Mar 2023 21:10:20 +0000 Subject: [PATCH 16/42] Translated using Weblate (Spanish) Currently translated at 31.4% (87 of 277 strings) Translation: Spowlo/strings Translate-URL: https://hosted.weblate.org/projects/spowlo/strings/es/ --- app/src/main/res/values-es/strings.xml | 50 ++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1dd86092..48de8237 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,4 +1,54 @@ Artista del álbum + Marcado + Sonido + Buscar actualizaciones + Artista + Cambia donde quieres descargar las canciones + Álbum + Mod AMOLED clonado + Mod AMOLED + Ocurrió un error en la búsqueda + Una descarga ya está en progreso. + Una nueva actualización está disponible! + Ocurrió un error al intentar actualizar la aplicación + Ocurrió un error al buscar acutalizaciones + Ocurrió un error al buscar la información de la canción + Ocurrió un error al intentar conectarse a la API de Spotify + Un mod de Spotify normal con saltos ilimitados, escucha en demanda, libre de anuncios y con las últimas funciones. Esto sustituye la versión original de Spotify (la que descargas desde la Play Store). + Canal Beta + Versión de la aplicación + Un mod de Spotify normal con saltos ilimitados, escucha en demanda, libre de anuncios y con las últimas funciones pero clonado. Esto significa que se instalará como una aplicación aparte de la original, podiendo tener ambas instaladas a la vez. + Proveedor de audio + Calidad de sonido + La llamada a la API de los Mods no fue bien... + Cancelar descarga + Mira el código fuente de Spowlo en GitHub! + Cambia como se ve la aplicación + Cambia ajustes relacionados con la API de Spotify… + Cambia el formato de tus descargas + Ajustes adicionales + Ocurrió un error al intentar descargar la canción + Acerca de + %.2f GB + Añadir + %.2f MB + Cambia tu configuración de internet + "Una versión ligera de Spotify sin anuncios y con saltos ilimitados. " + %1$d canción(es) + Estás seguro\? + Activa el uso de la aplicación en segundo plano para asegurarse de que todo funciona correctamente. + Ajusta tu descarga + Apariencia + Auto-actualizar + Álbum + Auto-actualización de la aplicación, canal de actualización… + Configuración de la batería + Cancelar + Cambia donde las descargas son guardadas + Apariencia + Directorio de los archivos de sonido + Formato del audio + Ocurrió un error desconocido. \ No newline at end of file From 1c329825499b5b19c3f76e0dfc1bdb530cc1b1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 20 Mar 2023 15:59:26 +0100 Subject: [PATCH 17/42] depends: added pagination and made a lot of changes for next pagination adding --- app/build.gradle.kts | 2 + .../spotify_api/SpotifyApiRequests.kt | 80 -------- .../data/paging/SpotifyApiMediator.kt | 1 + .../data/remote/SpotifyApiRequests.kt | 194 ++++++++++++++++++ .../{data/dtos => model}/SpotifyData.kt | 3 +- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 19 +- .../binders/SpotifyInfoBinder.kt | 4 +- .../binders/SpotifyPageBinder.kt | 4 +- .../pages/metadata_viewer/pages/AlbumPage.kt | 2 +- .../pages/metadata_viewer/pages/ArtistPage.kt | 2 +- .../metadata_viewer/pages/PlaylistViewPage.kt | 2 +- .../pages/metadata_viewer/pages/TrackPage.kt | 2 +- .../metadata_viewer/playlists/PlaylistPage.kt | 8 +- .../playlists/PlaylistPageViewModel.kt | 113 +++++++--- .../spowlo/ui/pages/searcher/SearcherPage.kt | 124 ++++++++++- .../pages/searcher/SearcherPageViewModel.kt | 2 +- gradle/libs.versions.toml | 10 +- 17 files changed, 439 insertions(+), 133 deletions(-) delete mode 100644 app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/paging/SpotifyApiMediator.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt rename app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/{data/dtos => model}/SpotifyData.kt (80%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8930de3f..eaa62cc7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -211,6 +211,8 @@ dependencies { implementation(libs.accompanist.pager.indicators) implementation(libs.accompanist.flowlayout) implementation(libs.accompanist.material) + implementation(libs.paging.compose) + implementation(libs.paging.runtime) implementation(libs.coil.kt.compose) diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt deleted file mode 100644 index 8cf5d9fe..00000000 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/SpotifyApiRequests.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.bobbyesp.spowlo.features.spotify_api - -import android.util.Log -import com.adamratzman.spotify.SpotifyAppApi -import com.adamratzman.spotify.models.Playlist -import com.adamratzman.spotify.models.SpotifyPublicUser -import com.adamratzman.spotify.models.SpotifySearchResult -import com.adamratzman.spotify.models.Token -import com.adamratzman.spotify.models.Track -import com.adamratzman.spotify.spotifyAppApi -import com.adamratzman.spotify.utils.Market -import com.bobbyesp.spowlo.BuildConfig -import kotlinx.coroutines.Job - -object SpotifyApiRequests { - - private val clientId = BuildConfig.CLIENT_ID - private val clientSecret = BuildConfig.CLIENT_SECRET - private var api: SpotifyAppApi? = null - private var token: Token? = null - - private var currentJob: Job? = null - - - //Pulls the clientId and clientSecret tokens and builds them into an object - - suspend fun buildApi() { - Log.d( - "SpotifyApiRequests", - "Building API with client ID: $clientId and client secret: $clientSecret" - ) - token = spotifyAppApi(clientId, clientSecret).build().token - api = spotifyAppApi(clientId, clientSecret, token!!) { - automaticRefresh = true - }.build() - } - - //Performs Spotify database query for queries related to user information. - suspend fun userSearch(userQuery: String): SpotifyPublicUser? { - return api!!.users.getProfile(userQuery) - } - - // Performs Spotify database query for queries related to track information. - suspend fun searchAllTypes(searchQuery: String): SpotifySearchResult { - kotlin.runCatching { - api!!.search.searchAllTypes(searchQuery, limit = 50, offset = 1, market = Market.ES) - }.onFailure { - Log.d("SpotifyApiRequests", "Error: ${it.message}") - return SpotifySearchResult() - }.onSuccess { - return it - } - return SpotifySearchResult() - } - - //search by id - suspend fun searchPlaylistById(id: String): Playlist? { - kotlin.runCatching { - api!!.playlists.getPlaylist(id, market = Market.ES) - }.onFailure { - Log.d("SpotifyApiRequests", "Error: ${it.message}") - return null - }.onSuccess { - return it - } - return null - } - - suspend fun searchTrackById(id: String): Track? { - kotlin.runCatching { - api!!.tracks.getTrack(id, market = Market.ES) - }.onFailure { - Log.d("SpotifyApiRequests", "Error: ${it.message}") - return null - }.onSuccess { - return it - } - return null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/paging/SpotifyApiMediator.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/paging/SpotifyApiMediator.kt new file mode 100644 index 00000000..0ad8766a --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/paging/SpotifyApiMediator.kt @@ -0,0 +1 @@ +package com.bobbyesp.spowlo.features.spotify_api.data.paging diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt new file mode 100644 index 00000000..a40b9cc9 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt @@ -0,0 +1,194 @@ +package com.bobbyesp.spowlo.features.spotify_api.data.remote + +import android.util.Log +import com.adamratzman.spotify.SpotifyAppApi +import com.adamratzman.spotify.models.Album +import com.adamratzman.spotify.models.Artist +import com.adamratzman.spotify.models.PagingObject +import com.adamratzman.spotify.models.Playlist +import com.adamratzman.spotify.models.SpotifyPublicUser +import com.adamratzman.spotify.models.SpotifySearchResult +import com.adamratzman.spotify.models.Token +import com.adamratzman.spotify.models.Track +import com.adamratzman.spotify.spotifyAppApi +import com.adamratzman.spotify.utils.Market +import com.bobbyesp.spowlo.BuildConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.Job +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SpotifyApiRequests { + + private const val clientId = BuildConfig.CLIENT_ID + private const val clientSecret = BuildConfig.CLIENT_SECRET + private var api: SpotifyAppApi? = null + private var token: Token? = null + + private var currentJob: Job? = null + + + //Pulls the clientId and clientSecret tokens and builds them into an object + + @Provides + @Singleton + suspend fun provideSpotifyApi(): SpotifyAppApi { + if (api == null) { + buildApi() + } + return api!! + } + + suspend fun buildApi() { + Log.d( + "SpotifyApiRequests", + "Building API with client ID: $clientId and client secret: $clientSecret" + ) + token = spotifyAppApi(clientId, clientSecret).build().token + api = spotifyAppApi(clientId, clientSecret, token!!) { + automaticRefresh = true + }.build() + } + + //Performs Spotify database query for queries related to user information. + suspend fun userSearch(userQuery: String): SpotifyPublicUser? { + return api!!.users.getProfile(userQuery) + } + + @Provides + @Singleton + suspend fun provideUserSearch(query: String): SpotifyPublicUser? { + return userSearch("bobbyesp") + } + + // Performs Spotify database query for queries related to track information. + suspend fun searchAllTypes(searchQuery: String): SpotifySearchResult { + kotlin.runCatching { + api!!.search.searchAllTypes(searchQuery, limit = 50, offset = 1, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return SpotifySearchResult() + }.onSuccess { + return it + } + return SpotifySearchResult() + } + + @Provides + @Singleton + suspend fun provideSearchAllTypes(query: String): SpotifySearchResult { + return searchAllTypes(query) + } + + suspend fun searchTracks(searchQuery: String): List { + kotlin.runCatching { + api!!.search.searchTrack(searchQuery, limit = 50, offset = 1, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return listOf() + }.onSuccess { + return it.items + } + return listOf() + } + + @Provides + @Singleton + suspend fun provideSearchTracks(query: String): List { + return searchTracks(query) + } + + suspend fun searchTracksForPaging(searchQuery: String, nextPageNumber: Int): PagingObject? { + kotlin.runCatching { + api!!.search.searchTrack(searchQuery, limit = 50, offset = nextPageNumber, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + }.onSuccess { + return it + } + return null + } + + @Provides + @Singleton + suspend fun provideSearchTracksForPaging(query: String, nextPageNumber: Int): PagingObject? { + return searchTracksForPaging(query, nextPageNumber) + } + + //search by id + suspend fun getPlaylistById(id: String): Playlist? { + kotlin.runCatching { + api!!.playlists.getPlaylist(id, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return null + }.onSuccess { + return it + } + return null + } + + @Provides + @Singleton + suspend fun provideGetPlaylistById(id: String): Playlist? { + return getPlaylistById(id) + } + + suspend fun getTrackById(id: String): Track? { + kotlin.runCatching { + api!!.tracks.getTrack(id, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return null + }.onSuccess { + return it + } + return null + } + + @Provides + @Singleton + suspend fun provideGetTrackById(id: String): Track? { + return getTrackById(id) + } + + suspend fun getArtistById(id: String): Artist? { + kotlin.runCatching { + api!!.artists.getArtist(id) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return null + }.onSuccess { + return it + } + return null + } + + @Provides + @Singleton + suspend fun providesGetArtistById(id: String): Artist? { + return getArtistById(id) + } + + suspend fun getAlbumById(id: String): Album? { + kotlin.runCatching { + api!!.albums.getAlbum(id, market = Market.ES) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + return null + }.onSuccess { + return it + } + return null + } + + @Provides + @Singleton + suspend fun providesGetAlbumById(id: String): Album? { + return getAlbumById(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/model/SpotifyData.kt similarity index 80% rename from app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt rename to app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/model/SpotifyData.kt index 95db5904..1e3c88a8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/dtos/SpotifyData.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/model/SpotifyData.kt @@ -1,4 +1,4 @@ -package com.bobbyesp.spowlo.features.spotify_api.data.dtos +package com.bobbyesp.spowlo.features.spotify_api.model import com.adamratzman.spotify.models.ReleaseDate @@ -7,6 +7,7 @@ data class SpotifyData( val name: String = "", val artists: List = emptyList(), val releaseDate: ReleaseDate? = null, + val playlistSize : Int? = 0, val type: SpotifyDataType = SpotifyDataType.TRACK ) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 504e2adb..a6d320b0 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -61,7 +61,7 @@ import com.bobbyesp.spowlo.BuildConfig import com.bobbyesp.spowlo.MainActivity import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.features.mod_downloader.data.remote.ModsDownloaderAPI -import com.bobbyesp.spowlo.features.spotify_api.SpotifyApiRequests +import com.bobbyesp.spowlo.features.spotify_api.data.remote.SpotifyApiRequests import com.bobbyesp.spowlo.ui.common.LocalWindowWidthState import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.common.animatedComposable @@ -429,25 +429,30 @@ fun InitialEntry( navDeepLink { // Want to go to "markdown_viewer/{markdownFileName}" uriPattern = - StringBuilder().append(navRootUrl).append(Route.PLAYLIST_PAGE) + StringBuilder().append(navRootUrl).append(Route.PLAYLIST_PAGE).append("/{type}") .append("/{id}").toString() - Log.d("TST_NAV", uriPattern!!) + } + val typeArg = navArgument("type") { + type = NavType.StringType } - val navArgument = navArgument("id") { + val idArg = navArgument("id") { type = NavType.StringType } val routeWithIdPattern: String = - StringBuilder().append(Route.PLAYLIST_PAGE).append("/{id}").toString() + StringBuilder().append(Route.PLAYLIST_PAGE).append("/{type}").append("/{id}").toString() animatedComposableVariant( routeWithIdPattern, - arguments = listOf(navArgument) + arguments = listOf(typeArg ,idArg) ) { backStackEntry -> val id = backStackEntry.arguments?.getString("id") ?: "SOMETHING WENT WRONG" + val type = backStackEntry.arguments?.getString("type") ?: "SOMETHING WENT WRONG" + PlaylistPage( onBackPressed, - id = id + id = id, + type = type ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt index 3ebfe43a..d25e4f13 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt @@ -1,10 +1,9 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders -import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.bobbyesp.spowlo.R -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyDataType +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType //make a composable that has as parameter a SpotifyData and returns the type of the data as a string with a string resource @Composable @@ -19,7 +18,6 @@ fun typeOfDataToString(type: SpotifyDataType): String { //Assign and return the type of the data from referred to the SpotifyDataType enum fun typeOfSpotifyDataType(type: String): SpotifyDataType { - Log.d("SpotifyDataType", "Type: $type") return when (type) { "track" -> SpotifyDataType.TRACK "album" -> SpotifyDataType.ALBUM diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt index b7ccf0bd..c7a7ffd1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt @@ -3,8 +3,8 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyDataType +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.AlbumPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.ArtistPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.PlaylistViewPage diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt index 5298847d..1ab5cb02 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt @@ -2,7 +2,7 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage @Composable diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt index cf84664a..947d31f8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt @@ -2,7 +2,7 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage @Composable diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index 7f6ffa9e..1fc08331 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -2,7 +2,7 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage @Composable diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index 68747dc8..1088a2df 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.R -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData import com.bobbyesp.spowlo.ui.common.AsyncImageImpl import com.bobbyesp.spowlo.ui.components.MarqueeText import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfDataToString diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt index 5be67e44..607ac1b6 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt @@ -20,17 +20,19 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.style.TextOverflow import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.pages.common.LoadingPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.SpotifyPageBinder +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun PlaylistPage( onBackPressed: () -> Unit, playlistPageViewModel: PlaylistPageViewModel = hiltViewModel(), - id: String + id: String, + type : String ) { val scope = rememberCoroutineScope() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -38,7 +40,7 @@ fun PlaylistPage( val viewState by playlistPageViewModel.viewStateFlow.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - playlistPageViewModel.loadData(id) + playlistPageViewModel.loadData(id, typeOfSpotifyDataType(type)) } with(viewState) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt index 237ddabd..37bd241a 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt @@ -1,9 +1,10 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists import androidx.lifecycle.ViewModel -import com.bobbyesp.spowlo.features.spotify_api.SpotifyApiRequests -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyData -import com.bobbyesp.spowlo.features.spotify_api.data.dtos.SpotifyDataType +import com.adamratzman.spotify.models.ReleaseDate +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType +import com.bobbyesp.spowlo.features.spotify_api.data.remote.SpotifyApiRequests import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -21,29 +22,93 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { ) suspend fun loadData(id: String, type: SpotifyDataType = SpotifyDataType.TRACK) { - kotlin.runCatching { - SpotifyApiRequests.searchTrackById(id) - }.onSuccess { track -> - mutableViewStateFlow.update { - it.copy( - state = PlaylistDataState.Loaded( - SpotifyData( - track!!.album.images[0].url, - track.name, - track.artists.map { it.name }, - track.album.releaseDate, - typeOfSpotifyDataType(track.type), + when (type) { + SpotifyDataType.TRACK -> { + kotlin.runCatching { + SpotifyApiRequests.getTrackById(id) + }.onSuccess { data -> + mutableViewStateFlow.update { + it.copy( + state = PlaylistDataState.Loaded( + SpotifyData( + data!!.album.images[0].url, + data.name, + data.artists.map { it.name }, + data.album.releaseDate, + type = typeOfSpotifyDataType(data.type), + ) + ) ) - ) - ) + } + }.onFailure { + mutableViewStateFlow.update { + it.copy( + state = PlaylistDataState.Error(Exception("Error while loading data")) + ) + } + } } - }.onFailure { - mutableViewStateFlow.update { - it.copy( - state = PlaylistDataState.Error(Exception("Error while loading data")) - ) + + SpotifyDataType.ALBUM -> { + kotlin.runCatching { + SpotifyApiRequests.getAlbumById(id) + }.onSuccess { data -> + mutableViewStateFlow.update { + it.copy( + state = PlaylistDataState.Loaded( + SpotifyData( + data!!.images[0].url, + data.name, + data.artists.map { it.name }, + data.releaseDate, + type = typeOfSpotifyDataType(data.type), + ) + ) + ) + } + }.onFailure { + mutableViewStateFlow.update { + it.copy( + state = PlaylistDataState.Error(Exception("Error while loading data")) + ) + } + } + } - } + SpotifyDataType.PLAYLIST -> { + kotlin.runCatching { + SpotifyApiRequests.getPlaylistById(id) + }.onSuccess { data -> + mutableViewStateFlow.update { + it.copy( + state = PlaylistDataState.Loaded( + SpotifyData( + data!!.images[0].url, + data.name, + listOf(data.owner.displayName ?: data.owner.id), + ReleaseDate(2000, 1, 1), + data.tracks.total, + typeOfSpotifyDataType(data.type), + ) + ) + ) + } + }.onFailure { + mutableViewStateFlow.update { + it.copy( + state = PlaylistDataState.Error(Exception("Error while loading data")) + ) + } + } + + } + + SpotifyDataType.ARTIST -> { + + } + } } -} \ No newline at end of file +} + + diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 1739ac3b..750dcebe 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -66,12 +66,12 @@ fun SearcherPage( .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { - SearcherPageImpl( + SearcherPageImpl( viewState = viewState, onValueChange = { query -> searcherPageViewModel.updateSearchText(query) }, - onItemClick = { id -> navController.navigate(Route.PLAYLIST_PAGE + "/" + id) } + onItemClick = { type, id -> navController.navigate(Route.PLAYLIST_PAGE + "/" + type + "/" + id) }, ) } LaunchedEffect(viewState.query) { @@ -81,12 +81,11 @@ fun SearcherPage( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearcherPageImpl( viewState: SearcherPageViewModel.ViewState, onValueChange: (String) -> Unit, - onItemClick: (String) -> Unit + onItemClick: (String, String) -> Unit, ) { Scaffold(modifier = Modifier.fillMaxSize()) { with(viewState) { @@ -251,7 +250,7 @@ fun SearcherPageImpl( songName = track.name, artists = artists.joinToString(", "), spotifyUrl = track.externalUrls.spotify ?: "", - onClick = { onItemClick(track.id) }, + onClick = { onItemClick(track.type ,track.id) }, type = typeOfDataToString( type = typeOfSpotifyDataType( track.type @@ -273,7 +272,7 @@ fun SearcherPageImpl( artists = playlist.owner.displayName ?: stringResource(R.string.unknown), spotifyUrl = playlist.externalUrls.spotify ?: "", - onClick = { onItemClick(playlist.id) }, + onClick = { onItemClick(playlist.type ,playlist.id) }, type = typeOfDataToString( type = typeOfSpotifyDataType( playlist.type @@ -298,6 +297,7 @@ fun SearcherPageImpl( } } } + } } } @@ -373,4 +373,114 @@ enum class FilterType { FilterType.EPISODES -> allItems.filterIsInstance() FilterType.SHOWS -> allItems.filterIsInstance() } -* */ \ No newline at end of file +* */ +// -------------------------------------------- + +/* +* val allItems = + mutableListOf() //TODO: Add the filters. Pagination should be done in the future + viewState.viewState.data.let { data -> + data.albums?.items?.let { allItems.addAll(it) } + data.artists?.items?.let { allItems.addAll(it) } + data.playlists?.items?.let { allItems.addAll(it) } + data.tracks?.items?.let { allItems.addAll(it) } + data.episodes?.items?.let { + allItems.addAll( + listOf( + it + ) + ) + } + data.shows?.items?.let { + allItems.addAll( + listOf( + it + ) + ) + } + if (data != null) { //You may think that this is not necessary, but it is + item { + Text( + text = stringResource(R.string.showing_results).format( + allItems.size + ), + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier + .padding(16.dp) + .alpha(0.7f), + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Start, + fontWeight = FontWeight.Bold + ) + + } + data.albums?.items?.forEachIndexed { index, album -> + item { + // TODO: Display the album item + } + } + data.artists?.items?.forEachIndexed { index, artist -> + item { + // TODO: Display the artist item + } + } + + data.tracks?.items?.forEachIndexed { index, track -> + item { + val artists: List = + track.artists.map { artist -> artist.name } + SearchingSongComponent( + artworkUrl = track.album.images[2].url, + songName = track.name, + artists = artists.joinToString(", "), + spotifyUrl = track.externalUrls.spotify ?: "", + onClick = { onItemClick(track.type ,track.id) }, + type = typeOfDataToString( + type = typeOfSpotifyDataType( + track.type + ) + ) + ) + HorizontalDivider( + modifier = Modifier.alpha(0.35f), + color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) + } + } + + data.playlists?.items?.forEachIndexed { index, playlist -> + item { + SearchingSongComponent( + artworkUrl = playlist.images[0].url, + songName = playlist.name, + artists = playlist.owner.displayName + ?: stringResource(R.string.unknown), + spotifyUrl = playlist.externalUrls.spotify ?: "", + onClick = { onItemClick(playlist.type ,playlist.id) }, + type = typeOfDataToString( + type = typeOfSpotifyDataType( + playlist.type + ) + ) + ) + HorizontalDivider( + modifier = Modifier.alpha(0.35f), + color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) + } + } + data.episodes?.items?.forEachIndexed { index, episode -> + item { + // TODO: Display the episode item + } + } + data.shows?.items?.forEachIndexed { index, show -> + item { + // TODO: Display the show item + } + } + } + } + * */ \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt index 1e61705c..0b61cc60 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt @@ -3,7 +3,7 @@ package com.bobbyesp.spowlo.ui.pages.searcher import android.util.Log import androidx.lifecycle.ViewModel import com.adamratzman.spotify.models.SpotifySearchResult -import com.bobbyesp.spowlo.features.spotify_api.SpotifyApiRequests +import com.bobbyesp.spowlo.features.spotify_api.data.remote.SpotifyApiRequests import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index debf1d93..f4b70260 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] accompanist = "0.29.2-rc" -androidGradlePlugin = "7.4.0" +androidGradlePlugin = "7.4.2" androidxComposeBom = "2023.01.00" androidxComposeCompiler = "1.4.0" androidxCore = "1.10.0-rc01" @@ -9,6 +9,9 @@ androidxAppCompat = "1.7.0-alpha02" androidxActivity = "1.6.1" markdownDependency = "0.3.2" navTransitions = "0.11.0-alpha" +androidxPaging = "3.1.1" +paginationCompose = "1.0.0-alpha18" + androidxLifecycle = "2.6.0" androidxNavigation = "2.5.3" @@ -64,6 +67,11 @@ accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist #Accompanist material component accompanist-material = { group = "com.google.accompanist", name = "accompanist-navigation-material", version.ref = "accompanist" } +#Paging3 +paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "androidxPaging" } +paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paginationCompose" } + + #Markdown parser markdown = { group = "com.github.jeziellago", name = "compose-markdown", version.ref = "markdownDependency" } From 3c074c8c593d56582eae0ad4b1c9e96cef309573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Thu, 23 Mar 2023 22:55:47 +0100 Subject: [PATCH 18/42] feat: Optimized searching, updated spotDL, changed Spotify data-bindings... --- .../ui/dialogs/DownloaderSettingsDialog.kt | 3 +- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 74 +------------------ .../binders/SpotifyInfoBinder.kt | 16 ++++ .../binders/SpotifyPageBinder.kt | 36 ++++++--- .../pages/metadata_viewer/pages/AlbumPage.kt | 4 +- .../pages/metadata_viewer/pages/ArtistPage.kt | 4 +- .../metadata_viewer/pages/PlaylistViewPage.kt | 4 +- .../pages/metadata_viewer/pages/TrackPage.kt | 22 +++--- .../metadata_viewer/playlists/PlaylistPage.kt | 5 +- .../playlists/PlaylistPageViewModel.kt | 30 +------- .../settings/format/AudioProviderDialog.kt | 20 ++--- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 10 ++- .../bobbyesp/spowlo/utils/PreferencesUtil.kt | 41 +++++----- app/src/main/res/values/strings.xml | 3 +- gradle/libs.versions.toml | 2 +- 15 files changed, 115 insertions(+), 159 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt index 9b984a84..1a8117d1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt @@ -1,6 +1,7 @@ package com.bobbyesp.spowlo.ui.dialogs import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -75,7 +76,7 @@ import com.bobbyesp.spowlo.utils.USE_SPOTIFY_CREDENTIALS import com.bobbyesp.spowlo.utils.USE_YT_METADATA import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class) @Composable fun DownloaderSettingsDialog( useDialog: Boolean = false, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index a6d320b0..6d81a6df 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -7,7 +7,6 @@ import android.provider.Settings import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.MutableTransitionState @@ -46,8 +45,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavType import androidx.navigation.compose.currentBackStackEntryAsState @@ -55,9 +52,7 @@ import androidx.navigation.compose.dialog import androidx.navigation.navArgument import androidx.navigation.navDeepLink import androidx.navigation.navigation -import com.bobbyesp.library.SpotDL import com.bobbyesp.spowlo.App -import com.bobbyesp.spowlo.BuildConfig import com.bobbyesp.spowlo.MainActivity import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.features.mod_downloader.data.remote.ModsDownloaderAPI @@ -92,10 +87,6 @@ import com.bobbyesp.spowlo.ui.pages.settings.format.SettingsFormatsPage import com.bobbyesp.spowlo.ui.pages.settings.general.GeneralSettingsPage import com.bobbyesp.spowlo.ui.pages.settings.spotify.SpotifySettingsPage import com.bobbyesp.spowlo.ui.pages.settings.updater.UpdaterPage -import com.bobbyesp.spowlo.utils.PreferencesUtil.getBoolean -import com.bobbyesp.spowlo.utils.PreferencesUtil.getString -import com.bobbyesp.spowlo.utils.SPOTDL -import com.bobbyesp.spowlo.utils.SPOTDL_UPDATE import com.bobbyesp.spowlo.utils.ToastUtil import com.bobbyesp.spowlo.utils.UpdateUtil import com.google.accompanist.navigation.animation.AnimatedNavHost @@ -107,7 +98,6 @@ import com.google.accompanist.navigation.material.rememberBottomSheetNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext private const val TAG = "InitialEntry" @@ -131,7 +121,6 @@ fun InitialEntry( navBackStackEntry?.destination?.parent?.route ?: Route.DownloaderNavi ) } - //navController.currentBackStack.value.getOrNull(1)?.destination?.route val shouldHideBottomNavBar = remember(navBackStackEntry) { navBackStackEntry?.destination?.hierarchy?.any { it.route == Route.SPOTIFY_SETUP } == true @@ -463,14 +452,14 @@ fun InitialEntry( } } - if (BuildConfig.DEBUG) LaunchedEffect(Unit) { + LaunchedEffect(Unit) { runCatching { - SpotifyApiRequests.buildApi() + SpotifyApiRequests.provideSpotifyApi() }.onFailure { it.printStackTrace() ToastUtil.makeToastSuspend(context.getString(R.string.spotify_api_error)) }.onSuccess { - val req = SpotifyApiRequests.searchAllTypes("Faded Alan Walker") + val req = SpotifyApiRequests.provideSearchAllTypes("Faded Alan Walker") Log.d("InitialEntry", "Name:" + req.tracks!![0].name) Log.d("InitialEntry", "Artist:" + req.tracks!![0].artists[0].name) Log.d("InitialEntry", "Album:" + req.tracks!![0].album.name) @@ -482,19 +471,6 @@ fun InitialEntry( } } - LaunchedEffect(Unit) { - if (!SPOTDL_UPDATE.getBoolean()) return@LaunchedEffect - runCatching { - withContext(Dispatchers.IO) { - val res = UpdateUtil.updateSpotDL() - if (res == SpotDL.UpdateStatus.DONE) { - ToastUtil.makeToastSuspend(context.getString(R.string.spotDl_uptodate) + " (${SPOTDL.getString()})") - } - } - }.onFailure { - it.printStackTrace() - } - } LaunchedEffect(Unit) { launch(Dispatchers.IO) { runCatching { @@ -522,53 +498,9 @@ fun InitialEntry( } if (showUpdateDialog) { - /*UpdateDialogImpl( - onDismissRequest = { - showUpdateDialog = false - updateJob?.cancel() - }, - title = latestRelease.name.toString(), - onConfirmUpdate = { - updateJob = scope.launch(Dispatchers.IO) { - runCatching { - UpdateUtil.downloadApk(latestRelease = latestRelease) - .collect { downloadStatus -> - currentDownloadStatus = downloadStatus - if (downloadStatus is UpdateUtil.DownloadStatus.Finished) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - launcher.launch(Manifest.permission.REQUEST_INSTALL_PACKAGES) - } - } - } - }.onFailure { - it.printStackTrace() - currentDownloadStatus = UpdateUtil.DownloadStatus.NotYet - ToastUtil.makeToastSuspend(context.getString(R.string.app_update_failed)) - return@launch - } - } - }, - releaseNote = latestRelease.body.toString(), - downloadStatus = currentDownloadStatus - )*/ UpdaterBottomDrawer(latestRelease = latestRelease) } } -@OptIn(ExperimentalAnimationApi::class) -private fun buildAnimationForward(scope: AnimatedContentScope): Boolean { - val isRoute = getStartingRoute(scope.initialState.destination) - val tsRoute = getStartingRoute(scope.targetState.destination) - - val isIndex = MainActivity.showInBottomNavigation.keys.indexOfFirst { it == isRoute } - val tsIndex = MainActivity.showInBottomNavigation.keys.indexOfFirst { it == tsRoute } - - return tsIndex == -1 || isRoute == tsRoute || tsIndex > isIndex -} - -private fun getStartingRoute(destination: NavDestination): String { - return destination.hierarchy.toList().let { it[it.lastIndex - 1] }.route.orEmpty() -} - //TODO: Separate the SettingsPage into a different NavGraph (like Seal) \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt index d25e4f13..d30e11a2 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyInfoBinder.kt @@ -2,6 +2,7 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource +import com.adamratzman.spotify.models.SimpleAlbum import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType @@ -25,4 +26,19 @@ fun typeOfSpotifyDataType(type: String): SpotifyDataType { "artist" -> SpotifyDataType.ARTIST else -> SpotifyDataType.TRACK } +} + +@Composable +fun dataStringToString(data: String, additional: String): String { + return when (typeOfSpotifyDataType(data)) { + SpotifyDataType.ALBUM -> stringResource(id = R.string.album) + " • " + additional + SpotifyDataType.ARTIST -> stringResource(id = R.string.artist) + " • " + additional + SpotifyDataType.PLAYLIST -> stringResource(id = R.string.playlist) + " • " + additional + SpotifyDataType.TRACK -> stringResource(id = R.string.track) + " • " + additional + } +} + +//RELEASE DATE TO STRING +fun releaseDateToString(album: SimpleAlbum): String { + TODO() } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt index c7a7ffd1..97123004 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt @@ -3,7 +3,10 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData +import com.adamratzman.spotify.models.Album +import com.adamratzman.spotify.models.Artist +import com.adamratzman.spotify.models.Playlist +import com.adamratzman.spotify.models.Track import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.AlbumPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.ArtistPage @@ -12,26 +15,41 @@ import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.TrackPage @Composable fun SpotifyPageBinder( - spotifyData: SpotifyData, - modifier : Modifier = Modifier + data: Any, + type: SpotifyDataType, + modifier: Modifier = Modifier ) { Column(modifier = modifier) { - when (spotifyData.type) { + when (type) { SpotifyDataType.ALBUM -> { - AlbumPage(spotifyData, modifier) + val album = data as? Album + album?.let { + AlbumPage(album, modifier) + } } SpotifyDataType.ARTIST -> { - ArtistPage(spotifyData, modifier) + val artist = data as? Artist + artist?.let { + ArtistPage(artist, modifier) + } } SpotifyDataType.PLAYLIST -> { - PlaylistViewPage(spotifyData, modifier) + val playlist = data as? Playlist + playlist?.let { + PlaylistViewPage(playlist, modifier) + } } SpotifyDataType.TRACK -> { - TrackPage(spotifyData, modifier) + val track = data as? Track + track?.let { + TrackPage(track, modifier) + } } } } -} \ No newline at end of file +} + +//data-type to SpotifyDataType \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt index 1ab5cb02..d960283f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt @@ -2,12 +2,12 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData +import com.adamratzman.spotify.models.Album import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage @Composable fun AlbumPage( - data: SpotifyData, + data: Album, modifier: Modifier ) { NotImplementedPage() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt index 947d31f8..213ec3dc 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt @@ -2,12 +2,12 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData +import com.adamratzman.spotify.models.Artist import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage @Composable fun ArtistPage( - data: SpotifyData, + data: Artist, modifier: Modifier ) { NotImplementedPage() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index 1fc08331..ddf2233f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -2,12 +2,12 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData +import com.adamratzman.spotify.models.Playlist import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage @Composable fun PlaylistViewPage( - data: SpotifyData, + data: Playlist, modifier: Modifier ) { NotImplementedPage() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index 1088a2df..94f81dde 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -21,21 +21,23 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.adamratzman.spotify.models.Track import com.bobbyesp.spowlo.R -import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData import com.bobbyesp.spowlo.ui.common.AsyncImageImpl import com.bobbyesp.spowlo.ui.components.MarqueeText -import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfDataToString +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.dataStringToString @Composable fun TrackPage( - data: SpotifyData, + data: Track, modifier: Modifier ) { val localConfig = LocalConfiguration.current - Column(modifier = modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 12.dp)) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 12.dp) + ) { Box( modifier = Modifier .clip(MaterialTheme.shapes.extraSmall) @@ -53,7 +55,7 @@ fun TrackPage( matchHeightConstraintsFirst = true ) .clip(MaterialTheme.shapes.small), - model = data.artworkUrl, + model = data.album.images[0].url, contentDescription = stringResource(id = R.string.track_artwork), contentScale = ContentScale.Crop, ) @@ -72,7 +74,7 @@ fun TrackPage( Spacer(modifier = Modifier.height(6.dp)) SelectionContainer { Text( - text = data.artists.joinToString(", ") { it }, + text = data.artists.joinToString(", ") { it.name }, style = MaterialTheme.typography.bodyMedium.copy( fontWeight = FontWeight.Bold ), @@ -82,7 +84,9 @@ fun TrackPage( Spacer(modifier = Modifier.height(6.dp)) SelectionContainer { Text( - text = typeOfDataToString(data.type) + " • " + data.releaseDate?.year.toString(), + text = dataStringToString( + data = data.type, + additional = data.album.releaseDate!!.year.toString()), //TODO: CHANGE THIS TO NOT BE HARDCODED style = MaterialTheme.typography.bodySmall, modifier = Modifier.alpha(alpha = 0.8f) ) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt index 607ac1b6..6a0353b2 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.style.TextOverflow import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.pages.common.LoadingPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.SpotifyPageBinder @@ -78,7 +77,7 @@ fun PlaylistPage( .fillMaxSize()) { item{ Box(Modifier.animateItemPlacement()) { - SpotifyPageBinder(spotifyData = state.data, modifier = Modifier) + SpotifyPageBinder(data = state.data, type = typeOfSpotifyDataType(type), modifier = Modifier) } } } @@ -91,7 +90,7 @@ fun PlaylistPage( sealed class PlaylistDataState { object Loading : PlaylistDataState() class Error(val error: Exception) : PlaylistDataState() - class Loaded(val data: SpotifyData) : PlaylistDataState() + class Loaded(val data: Any) : PlaylistDataState() } class ToolbarOptions( diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt index 37bd241a..6681cbe3 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt @@ -1,11 +1,8 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists import androidx.lifecycle.ViewModel -import com.adamratzman.spotify.models.ReleaseDate -import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyData -import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType import com.bobbyesp.spowlo.features.spotify_api.data.remote.SpotifyApiRequests -import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType +import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -30,13 +27,7 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { mutableViewStateFlow.update { it.copy( state = PlaylistDataState.Loaded( - SpotifyData( - data!!.album.images[0].url, - data.name, - data.artists.map { it.name }, - data.album.releaseDate, - type = typeOfSpotifyDataType(data.type), - ) + data!! ) ) } @@ -56,13 +47,7 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { mutableViewStateFlow.update { it.copy( state = PlaylistDataState.Loaded( - SpotifyData( - data!!.images[0].url, - data.name, - data.artists.map { it.name }, - data.releaseDate, - type = typeOfSpotifyDataType(data.type), - ) + data!! ) ) } @@ -83,14 +68,7 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { mutableViewStateFlow.update { it.copy( state = PlaylistDataState.Loaded( - SpotifyData( - data!!.images[0].url, - data.name, - listOf(data.owner.displayName ?: data.owner.id), - ReleaseDate(2000, 1, 1), - data.tracks.total, - typeOfSpotifyDataType(data.type), - ) + data!! ) ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/AudioProviderDialog.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/AudioProviderDialog.kt index 86e5ef2d..0310af16 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/AudioProviderDialog.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/AudioProviderDialog.kt @@ -13,15 +13,15 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.components.ConfirmButton -import com.bobbyesp.spowlo.ui.components.SingleChoiceItemWithIcon +import com.bobbyesp.spowlo.ui.components.SingleChoiceItem import com.bobbyesp.spowlo.utils.AUDIO_PROVIDER import com.bobbyesp.spowlo.utils.PreferencesUtil @@ -45,16 +45,16 @@ fun AudioProviderDialog( style = MaterialTheme.typography.bodyLarge ) LazyColumn { - for (i in 0..1) { + for (i in 0..3) { item { - SingleChoiceItemWithIcon( + SingleChoiceItem( text = PreferencesUtil.getAudioProviderDesc(i), selected = audioProvider == i, - icon = PreferencesUtil.getAudioProviderIcon(i), onClick = { audioProvider = i - } + }, ) + } } } @@ -68,9 +68,11 @@ fun AudioProviderDialog( } ) }, - dismissButton = { TextButton(onClick = { onDismissRequest() }) { - Text(text = stringResource(id = R.string.dismiss)) - } }, + dismissButton = { + TextButton(onClick = { onDismissRequest() }) { + Text(text = stringResource(id = R.string.dismiss)) + } + }, ) } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index 944768e3..5d52ccda 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -203,8 +203,12 @@ object DownloaderUtil { private fun SpotDLRequest.addAudioProvider(): SpotDLRequest = this.apply { when (PreferencesUtil.getAudioProvider()) { 0 -> null - 1 -> addOption("--provider", "youtube-music") - 2 -> addOption("--provider", "youtube") + 1 -> { + addOption("--audio", "youtube-music") + addOption("youtube") + } + 2 -> addOption("--audio", "youtube-music") + 3 -> addOption("--audio", "youtube") } } @@ -292,7 +296,7 @@ object DownloaderUtil { } }.onSuccess { response -> return when { - response.output.contains("LookupError") -> Result.failure(Throwable("A LookupError occurred. The song wasn't found.")) + response.output.contains("LookupError") -> Result.failure(Throwable("A LookupError occurred. The song wasn't found. Try changing the audio provider in the settings and also disabling the 'Don't filter results' option.")) response.output.contains("YT-DLP") -> Result.failure(Throwable("An error occurred to yt-dlp while downloading the song. Please, report this issue in GitHub.")) else -> onFinishDownloading( this, diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt index a10db320..886f0c41 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt @@ -173,8 +173,9 @@ object PreferencesUtil { fun getAudioProviderDesc(audioProviderInt: Int = getAudioProvider()): String { return when (audioProviderInt){ 0 -> context.getString(R.string.default_option) - 1 -> "Youtube Music" - 2 -> "Youtube" + 1 -> context.getString(R.string.both) + 2 -> "Youtube Music" + 3 -> "Youtube" else -> "Youtube Music" } } @@ -182,8 +183,8 @@ object PreferencesUtil { @Composable fun getAudioProviderIcon(audioProviderInt: Int = getAudioProvider()): ImageVector { return when (audioProviderInt){ - 0 -> LocalAsset(id = R.drawable.youtube_music_icons8) - 1 -> LocalAsset(id = R.drawable.icons8_youtube) + 2 -> LocalAsset(id = R.drawable.youtube_music_icons8) + 3 -> LocalAsset(id = R.drawable.icons8_youtube) else -> LocalAsset(id = R.drawable.youtube_music_icons8) } } @@ -191,22 +192,22 @@ object PreferencesUtil { fun getAudioQualityDesc(audioQualityStr: Int = getAudioQuality()): String { return when (audioQualityStr) { 0 -> context.getString(R.string.not_specified) - 1 -> "8k" - 2 -> "16k" - 3 -> "24k" - 4 -> "32k" - 5 -> "40k" - 6 -> "48k" - 7 -> "64k" - 8 -> "80k" - 9 -> "96k" - 10 -> "112k" - 11 -> "128k" - 12 -> "160k" - 13 -> "192k" - 14 -> "224k" - 15 -> "256k" - 16 -> "320k" + 1 -> "8 kbps" + 2 -> "16 kbps" + 3 -> "24 kbps" + 4 -> "32 kbps" + 5 -> "40 kbps" + 6 -> "48 kbps" + 7 -> "64 kbps" + 8 -> "80 kbps" + 9 -> "96 kbps" + 10 -> "112 kbps" + 11 -> "128 kbps" + 12 -> "160 kbps" + 13 -> "192 kbps" + 14 -> "224 kbps" + 15 -> "256 kbps" + 16 -> "320 kbps" 17 -> context.getString(R.string.not_convert) else -> "auto" } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30fe3ce4..ebf4980e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -262,7 +262,7 @@ What would you like to download? No results were found Single - Loading the page... + Loading the page… Not specified Default Don\'t convert @@ -278,4 +278,5 @@ Shwowing %1$d results Sorry, this page is not yet implemented ;( Go back + Both \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f4b70260..fa7af987 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ androidxHiltNavigationCompose = "1.0.0" androidxTestExt = "1.1.5" -spotdlAndroidVersion = "fc391374c9" +spotdlAndroidVersion = "074aea43bb" spotifyApiKotlinVersion = "3.8.8" crashHandlerVersion = "2.0.2" From d3adf7ab15def65864599375cde99e0b4c515e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Fri, 24 Mar 2023 16:57:57 +0100 Subject: [PATCH 19/42] bugfix: Added a network verifier for updates and also mod api calling. Also added new download bottom drawer route --- .../data/remote/ModsDownloaderAPI.kt | 55 +++++------- .../com/bobbyesp/spowlo/ui/common/Route.kt | 5 +- .../ui/dialogs/DownloaderSettingsDialog.kt | 46 ++-------- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 36 +++++--- .../ui/pages/downloader/DownloaderPage.kt | 88 ++++++++++--------- .../com/bobbyesp/spowlo/utils/UpdateUtil.kt | 4 +- app/src/main/res/values/strings.xml | 1 + 7 files changed, 102 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPI.kt b/app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPI.kt index 629b732c..2077e547 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPI.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPI.kt @@ -12,12 +12,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json -import okhttp3.Call -import okhttp3.Callback import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.Response -import okio.IOException import java.io.File import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -32,48 +28,39 @@ object ModsDownloaderAPI { private val client = OkHttpClient() const val TAG = "APKsDownloaderAPI" - private val requestAPIResponse = - Request.Builder() - .url(BASE_URL + ENDPOINT) - .build() + private val requestAPIResponse = Request.Builder().url(BASE_URL + ENDPOINT).build() @CheckResult - private suspend fun getAPIResponse(): Result{ - return suspendCoroutine { - client.newCall(requestAPIResponse).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - it.resumeWith(Result.failure(e)) + suspend fun getAPIResponse(): Result { + return suspendCoroutine { continuation -> + client.newCall(requestAPIResponse).enqueue(object : okhttp3.Callback { + override fun onFailure(call: okhttp3.Call, e: java.io.IOException) { + continuation.resumeWith(Result.failure(e)) } - override fun onResponse(call: Call, response: Response) { - val responseData = response.body.string() - val apiResponse = jsonFormat.decodeFromString(APIResponseDto.serializer(), responseData) - response.body.close() - it.resume(Result.success(apiResponse)) + override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { + val body = response.body?.string() + if (body != null) { + val apiResponseDto = jsonFormat.decodeFromString(APIResponseDto.serializer(), body) + continuation.resume(Result.success(apiResponseDto)) + } else { + continuation.resumeWith(Result.failure(Exception("Body is null"))) + } } }) } } - suspend fun callModsAPI(): Result { - return getAPIResponse() - } - - private fun Context.getSpotifyAPK() = - File(getExternalFilesDir("apk"), "Spotify_Spowlo_Mod.apk") + private fun Context.getSpotifyAPK() = File(getExternalFilesDir("apk"), "Spotify_Spowlo_Mod.apk") suspend fun downloadPackage( - context: Context = App.context, - apiResponseDto: APIResponseDto, - listName: String, - index: Int - ): Flow { - withContext(Dispatchers.IO){ + context: Context = App.context, apiResponseDto: APIResponseDto, listName: String, index: Int + ): Flow { + withContext(Dispatchers.IO) { var selectedList = emptyList() - when(listName){ + when (listName) { "Regular" -> selectedList = apiResponseDto.apps.Regular "Amoled" -> selectedList = apiResponseDto.apps.AMOLED "Regular_Cloned" -> selectedList = apiResponseDto.apps.Regular_Cloned @@ -83,9 +70,7 @@ object ModsDownloaderAPI { val file = context.getSpotifyAPK() - val request = Request.Builder() - .url(selectedList[index].link) - .build() + val request = Request.Builder().url(selectedList[index].link).build() try { val response = client.newCall(request).execute() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt index 5bf263a5..bcf77ef5 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt @@ -2,6 +2,7 @@ package com.bobbyesp.spowlo.ui.common object Route { + const val DOWNLOADER_SHEET = "downloader_sheet" const val NavGraph = "nav_graph" const val SearcherNavi = "searcher_navi" const val DownloaderNavi = "downloader_navi" @@ -36,16 +37,12 @@ object Route { const val DOWNLOAD_DIRECTORY = "download_directory" const val CREDITS = "credits" const val LANGUAGES = "languages" - const val DARK_THEME = "dark_theme" const val DOWNLOAD_QUEUE = "queue" const val DOWNLOAD_FORMAT = "download_format" const val NETWORK_PREFERENCES = "network_preferences" const val COOKIE_PROFILE = "cookie_profile" const val COOKIE_GENERATOR_WEBVIEW = "cookie_webview" - const val TASK_HASHCODE = "task_hashcode" - const val TEMPLATE_ID = "template_id" - //DIALOGS const val AUDIO_QUALITY_DIALOG = "audio_quality_dialog" const val AUDIO_FORMAT_DIALOG = "audio_format_dialog" diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt index 1a8117d1..08888705 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt @@ -1,9 +1,7 @@ package com.bobbyesp.spowlo.ui.dialogs -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,22 +10,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AudioFile import androidx.compose.material.icons.outlined.Cancel import androidx.compose.material.icons.outlined.Dataset import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.DownloadDone -import androidx.compose.material.icons.outlined.HighQuality -import androidx.compose.material.icons.outlined.Key -import androidx.compose.material.icons.outlined.Person -import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -41,7 +32,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -49,12 +39,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bobbyesp.spowlo.R -import com.bobbyesp.spowlo.ui.common.intState -import com.bobbyesp.spowlo.ui.components.AudioFilterChip import com.bobbyesp.spowlo.ui.components.BottomDrawer -import com.bobbyesp.spowlo.ui.components.ButtonChip import com.bobbyesp.spowlo.ui.components.DismissButton -import com.bobbyesp.spowlo.ui.components.DrawerSheetSubtitle import com.bobbyesp.spowlo.ui.components.FilledButtonWithIcon import com.bobbyesp.spowlo.ui.components.OutlinedButtonWithIcon import com.bobbyesp.spowlo.ui.pages.settings.format.AudioFormatDialog @@ -62,7 +48,6 @@ import com.bobbyesp.spowlo.ui.pages.settings.format.AudioQualityDialog import com.bobbyesp.spowlo.ui.pages.settings.spotify.SpotifyClientIDDialog import com.bobbyesp.spowlo.ui.pages.settings.spotify.SpotifyClientSecretDialog import com.bobbyesp.spowlo.utils.COOKIES -import com.bobbyesp.spowlo.utils.CUSTOM_COMMAND import com.bobbyesp.spowlo.utils.DONT_FILTER_RESULTS import com.bobbyesp.spowlo.utils.GEO_BYPASS import com.bobbyesp.spowlo.utils.ORIGINAL_AUDIO @@ -70,13 +55,13 @@ import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.PreferencesUtil.templateStateFlow import com.bobbyesp.spowlo.utils.SKIP_INFO_FETCH import com.bobbyesp.spowlo.utils.SYNCED_LYRICS -import com.bobbyesp.spowlo.utils.TEMPLATE_ID import com.bobbyesp.spowlo.utils.USE_CACHING import com.bobbyesp.spowlo.utils.USE_SPOTIFY_CREDENTIALS import com.bobbyesp.spowlo.utils.USE_YT_METADATA -import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class, + ExperimentalFoundationApi::class +) @Composable fun DownloaderSettingsDialog( useDialog: Boolean = false, @@ -89,9 +74,6 @@ fun DownloaderSettingsDialog( ) { val settings = PreferencesUtil - var customCommand by remember { mutableStateOf(PreferencesUtil.getValue(CUSTOM_COMMAND)) } - var selectedTemplateId by TEMPLATE_ID.intState - var preserveOriginalAudio by remember { mutableStateOf( settings.getValue( @@ -165,34 +147,19 @@ fun DownloaderSettingsDialog( val scrollState = rememberLazyListState() val scope = rememberCoroutineScope() - LaunchedEffect(templateList.size, customCommand) { - if (customCommand) { - templateList.indexOfFirst { it.id == selectedTemplateId } - .run { if (!equals(-1)) scrollState.scrollToItem(this) } - } - } - - val updatePreferences = { - scope.launch { - settings.updateValue(CUSTOM_COMMAND, customCommand) - settings.encodeInt(TEMPLATE_ID, selectedTemplateId) - } - } val downloadButtonCallback = { - updatePreferences() hide() confirm() } val requestMetadata = { - updatePreferences() hide() onRequestMetadata() } val sheetContent: @Composable () -> Unit = { - Column { + /*Column { Text( text = stringResource(R.string.settings_before_download_text), style = MaterialTheme.typography.bodySmall, @@ -375,6 +342,7 @@ fun DownloaderSettingsDialog( ) } } + */ } if (!useDialog) { //TODO: Change this UI @@ -401,7 +369,9 @@ fun DownloaderSettingsDialog( ) } sheetContent() + val state = rememberLazyListState() + LaunchedEffect(drawerState.isVisible) { state.scrollToItem(1) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 6d81a6df..f3d23a7f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -87,6 +87,7 @@ import com.bobbyesp.spowlo.ui.pages.settings.format.SettingsFormatsPage import com.bobbyesp.spowlo.ui.pages.settings.general.GeneralSettingsPage import com.bobbyesp.spowlo.ui.pages.settings.spotify.SpotifySettingsPage import com.bobbyesp.spowlo.ui.pages.settings.updater.UpdaterPage +import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.ToastUtil import com.bobbyesp.spowlo.utils.UpdateUtil import com.google.accompanist.navigation.animation.AnimatedNavHost @@ -268,7 +269,7 @@ fun InitialEntry( DownloaderPage( navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY) }, navigateToSettings = { navController.navigate(Route.MORE_OPTIONS_HOME) }, - navigateToPlaylistPage = { navController.navigate(Route.PLAYLIST) }, + navigateToDownloaderSheet = { navController.navigate(Route.DOWNLOADER_SHEET) }, onSongCardClicked = { navController.navigate(Route.PLAYLIST_METADATA_PAGE) }, @@ -403,6 +404,10 @@ fun InitialEntry( navController ) } + + bottomSheet(Route.DOWNLOADER_SHEET) { + } + } //Can add the downloads history bottom sheet here using `val downloadsHistoryViewModel = hiltViewModel()` @@ -421,6 +426,8 @@ fun InitialEntry( StringBuilder().append(navRootUrl).append(Route.PLAYLIST_PAGE).append("/{type}") .append("/{id}").toString() } + + //We create the arguments for the route val typeArg = navArgument("type") { type = NavType.StringType } @@ -428,8 +435,13 @@ fun InitialEntry( val idArg = navArgument("id") { type = NavType.StringType } + + + //We build the route with the type of the destination and the id of it val routeWithIdPattern: String = StringBuilder().append(Route.PLAYLIST_PAGE).append("/{type}").append("/{id}").toString() + + //We create the composable with the route and the arguments animatedComposableVariant( routeWithIdPattern, arguments = listOf(typeArg ,idArg) @@ -444,8 +456,6 @@ fun InitialEntry( type = type ) } - - } } } @@ -471,10 +481,10 @@ fun InitialEntry( } } + LaunchedEffect(Unit) { - launch(Dispatchers.IO) { + if (!PreferencesUtil.isNetworkAvailableForDownload()) launch(Dispatchers.IO) { runCatching { - //TODO: Add check for updates of spotDL UpdateUtil.checkForUpdate()?.let { latestRelease = it showUpdateDialog = true @@ -484,19 +494,23 @@ fun InitialEntry( } }.onFailure { it.printStackTrace() + ToastUtil.makeToastSuspend(context.getString(R.string.update_check_failed)) } } } LaunchedEffect(Unit) { - Log.d(TAG, "InitialEntry: Checking for updates") - ModsDownloaderAPI.callModsAPI().onFailure { - ToastUtil.makeToastSuspend(App.context.getString(R.string.api_call_failed)) - }.onSuccess { - modsDownloaderViewModel.updateApiResponse(it) - } + Log.d(TAG, "InitialEntry: Checking for mod updates") + if (!PreferencesUtil.isNetworkAvailableForDownload()) ModsDownloaderAPI.getAPIResponse() + .onSuccess { + Log.d(TAG, "InitialEntry: Mods API call success") + modsDownloaderViewModel.updateApiResponse(it) + }.onFailure { + ToastUtil.makeToastSuspend(App.context.getString(R.string.api_call_failed)) + } } + if (showUpdateDialog) { UpdaterBottomDrawer(latestRelease = latestRelease) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index 2b8e6e19..2ad5ca3a 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -97,12 +97,14 @@ import com.google.accompanist.permissions.rememberPermissionState @Composable @OptIn( - ExperimentalPermissionsApi::class, ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class + ExperimentalPermissionsApi::class, + ExperimentalComposeUiApi::class, + ExperimentalMaterialApi::class ) fun DownloaderPage( navigateToSettings: () -> Unit = {}, navigateToDownloads: () -> Unit = {}, - navigateToPlaylistPage: () -> Unit = {}, + navigateToDownloaderSheet: () -> Unit = {}, onSongCardClicked: () -> Unit = {}, onNavigateToTaskList: () -> Unit = {}, navigateToMods: () -> Unit = {}, @@ -139,10 +141,7 @@ fun DownloaderPage( } val downloadCallback = { - if (CONFIGURE.getBoolean()) downloaderViewModel.showDialog( - scope, - useDialog - ) + if (CONFIGURE.getBoolean()) navigateToDownloaderSheet() else checkPermissionOrDownload() keyboardController?.hide() } @@ -178,7 +177,10 @@ fun DownloaderPage( viewState = viewState, errorState = errorState, downloadCallback = { downloadCallback() }, - navigateToSettings = navigateToSettings, + navigateToSettings = { + navigateToSettings() + keyboardController?.hide() + }, navigateToDownloads = navigateToDownloads, navigateToMods = navigateToMods, onNavigateToTaskList = onNavigateToTaskList, @@ -235,39 +237,41 @@ fun DownloaderPageImplementation( isPreview: Boolean = false, content: @Composable () -> Unit ) { - Scaffold(modifier = Modifier.fillMaxSize(), topBar = { - TopAppBar(title = {}, modifier = Modifier.padding(horizontal = 8.dp), - navigationIcon = { - IconButton(onClick = { navigateToSettings() }) { - Icon( - imageVector = Icons.Outlined.FormatListBulleted, - contentDescription = stringResource(id = R.string.show_more_actions) - ) - } - }, actions = { - IconButton(onClick = { navigateToMods() }) { - Icon( - imageVector = LocalAsset(id = R.drawable.spotify_logo), - contentDescription = stringResource(id = R.string.mods_downloader) - ) - } + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar(title = {}, modifier = Modifier.padding(horizontal = 8.dp), + navigationIcon = { + IconButton(onClick = { navigateToSettings() }) { + Icon( + imageVector = Icons.Outlined.FormatListBulleted, + contentDescription = stringResource(id = R.string.show_more_actions) + ) + } + }, actions = { + IconButton(onClick = { navigateToMods() }) { + Icon( + imageVector = LocalAsset(id = R.drawable.spotify_logo), + contentDescription = stringResource(id = R.string.mods_downloader) + ) + } - IconButton(onClick = { navigateToDownloads() }) { - Icon( - imageVector = Icons.Outlined.Subscriptions, - contentDescription = stringResource(id = R.string.downloads_history) - ) - } - }) - }, floatingActionButton = { - FABs( - modifier = with(Modifier) { if (showDownloadProgress) this else this.imePadding() }, - downloadCallback = downloadCallback, - pasteCallback = pasteCallback, - cancelCallback = cancelCallback, - isDownloading = downloaderState is Downloader.State.DownloadingSong, - ) - }) { + IconButton(onClick = { navigateToDownloads() }) { + Icon( + imageVector = Icons.Outlined.Subscriptions, + contentDescription = stringResource(id = R.string.downloads_history) + ) + } + }) + }, floatingActionButton = { + FABs( + modifier = with(Modifier) { if (showDownloadProgress) this else this.imePadding() }, + downloadCallback = downloadCallback, + pasteCallback = pasteCallback, + cancelCallback = cancelCallback, + isDownloading = downloaderState is Downloader.State.DownloadingSong, + ) + }) { Column( modifier = Modifier .padding(it) @@ -322,8 +326,7 @@ fun DownloaderPageImplementation( song = info, progress = progress, modifier = Modifier.padding(top = 16.dp, bottom = 4.dp), - isLyrics = info.lyrics?.isNotEmpty() - ?: false, + isLyrics = hasLyrics, isExplicit = info.explicit, onClick = { onSongCardClicked() } ) @@ -377,7 +380,7 @@ fun FABs( downloadCallback: () -> Unit = {}, pasteCallback: () -> Unit = {}, cancelCallback: () -> Unit = {}, - isDownloading : Boolean = false + isDownloading: Boolean = false ) { Column( modifier = modifier.padding(6.dp), horizontalAlignment = Alignment.End @@ -441,7 +444,6 @@ fun InputUrl( maxLines = 3, trailingIcon = { if (url.isNotEmpty()) ClearButton { onValueChange("") } -// else PasteUrlButton { onPaste() } }, keyboardActions = KeyboardActions(onDone = { softwareKeyboardController?.hide() focusManager.moveFocus(FocusDirection.Down) diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/UpdateUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/UpdateUtil.kt index 41dbf50a..9f1fdc39 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/UpdateUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/UpdateUtil.kt @@ -102,7 +102,8 @@ object UpdateUtil { private suspend fun getLatestRelease(): LatestRelease { return suspendCoroutine { continuation -> - client.newCall(requestForReleases).enqueue(object : Callback { + client.newCall(requestForReleases) + .enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val responseData = response.body.string() // val latestRelease = jsonFormat.decodeFromString(responseData) @@ -118,7 +119,6 @@ object UpdateUtil { response.body.close() continuation.resume(latestRelease) } - override fun onFailure(call: Call, e: IOException) { continuation.resumeWithException(e) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebf4980e..e97e8728 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -279,4 +279,5 @@ Sorry, this page is not yet implemented ;( Go back Both + The app updates checker failed \ No newline at end of file From 4021771671339009b6d79b1ff80456ac5869db55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Fri, 24 Mar 2023 17:02:46 +0100 Subject: [PATCH 20/42] bugfix: fixed the verifier lol --- .../data/remote/ModsDownloaderAPI.kt | 32 +++++++++++-------- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 4 +-- .../bobbyesp/spowlo/utils/PreferencesUtil.kt | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPI.kt b/app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPI.kt index 2077e547..7e810b3e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPI.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/mod_downloader/data/remote/ModsDownloaderAPI.kt @@ -12,10 +12,15 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json +import okhttp3.Call +import okhttp3.Callback import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.Response +import okio.IOException import java.io.File import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine object ModsDownloaderAPI { @@ -34,21 +39,20 @@ object ModsDownloaderAPI { @CheckResult suspend fun getAPIResponse(): Result { return suspendCoroutine { continuation -> - client.newCall(requestAPIResponse).enqueue(object : okhttp3.Callback { - override fun onFailure(call: okhttp3.Call, e: java.io.IOException) { - continuation.resumeWith(Result.failure(e)) - } - - override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { - val body = response.body?.string() - if (body != null) { - val apiResponseDto = jsonFormat.decodeFromString(APIResponseDto.serializer(), body) - continuation.resume(Result.success(apiResponseDto)) - } else { - continuation.resumeWith(Result.failure(Exception("Body is null"))) + client.newCall(requestAPIResponse).enqueue(object : Callback { + + override fun onResponse(call: Call, response: Response) { + val responseData = response.body.string() + val apiResponse = + jsonFormat.decodeFromString(APIResponseDto.serializer(), responseData) + response.body.close() + continuation.resume(Result.success(apiResponse)) + } + + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) } - } - }) + }) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index f3d23a7f..e0e99e3f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -483,7 +483,7 @@ fun InitialEntry( LaunchedEffect(Unit) { - if (!PreferencesUtil.isNetworkAvailableForDownload()) launch(Dispatchers.IO) { + if (PreferencesUtil.isNetworkAvailableForDownload()) launch(Dispatchers.IO) { runCatching { UpdateUtil.checkForUpdate()?.let { latestRelease = it @@ -501,7 +501,7 @@ fun InitialEntry( LaunchedEffect(Unit) { Log.d(TAG, "InitialEntry: Checking for mod updates") - if (!PreferencesUtil.isNetworkAvailableForDownload()) ModsDownloaderAPI.getAPIResponse() + if (PreferencesUtil.isNetworkAvailableForDownload()) ModsDownloaderAPI.getAPIResponse() .onSuccess { Log.d(TAG, "InitialEntry: Mods API call success") modsDownloaderViewModel.updateApiResponse(it) diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt index 886f0c41..1a24fc43 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt @@ -214,7 +214,7 @@ object PreferencesUtil { } fun isNetworkAvailableForDownload() = - CELLULAR_DOWNLOAD.getBoolean() || !App.connectivityManager.isActiveNetworkMetered + !App.connectivityManager.isActiveNetworkMetered //CELLULAR_DOWNLOAD.getBoolean() || fun isAutoUpdateEnabled() = AUTO_UPDATE.getBoolean(!isFDroidBuild()) From f70460e75ea428ac6069716f775dd26dfcc72ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Fri, 24 Mar 2023 22:42:01 +0100 Subject: [PATCH 21/42] beat: Changed settings main page items to a new ones --- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 8 +- .../spowlo/ui/pages/settings/SettingsPage.kt | 198 ++++++++++-------- 2 files changed, 119 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index e0e99e3f..8cdd08c2 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize @@ -199,7 +200,8 @@ fun InitialEntry( NavigationBar( modifier = Modifier .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) - .navigationBarsPadding(), + .navigationBarsPadding() + .height(72.dp), ) { MainActivity.showInBottomNavigation.forEach { (route, icon) -> val text = when (route) { @@ -237,7 +239,8 @@ fun InitialEntry( Text( text = text, style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(top = 6.dp) ) }) } @@ -462,6 +465,7 @@ fun InitialEntry( } } + //INIT SPOTIFY API LaunchedEffect(Unit) { runCatching { SpotifyApiRequests.provideSpotifyApi() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt index 4819ed3c..9e3aec74 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt @@ -12,9 +12,13 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Aod import androidx.compose.material.icons.filled.AudioFile @@ -27,6 +31,7 @@ import androidx.compose.material.icons.outlined.Cookie import androidx.compose.material.icons.rounded.EnergySavingsLeaf import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -34,18 +39,20 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.LocalDarkTheme import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.PreferencesHintCard -import com.bobbyesp.spowlo.ui.components.SettingItem import com.bobbyesp.spowlo.ui.components.SettingTitle import com.bobbyesp.spowlo.ui.components.SmallTopAppBar +import com.bobbyesp.spowlo.ui.components.settings.SettingsItemNew import com.bobbyesp.spowlo.ui.pages.settings.about.LocalAsset @OptIn(ExperimentalMaterial3Api::class) @@ -83,7 +90,8 @@ fun SettingsPage(navController: NavController) { }) { LazyColumn( - modifier = Modifier.padding(it) + modifier = Modifier.padding(it), + contentPadding = PaddingValues(horizontal = 16.dp) ) { item { SettingTitle(text = stringResource(id = R.string.settings)) @@ -114,112 +122,132 @@ fun SettingsPage(navController: NavController) { } } item { - SettingItem( - title = stringResource(id = R.string.general_settings), - description = stringResource( - id = R.string.general_settings_desc - ), - icon = Icons.Filled.SettingsApplications - ) { + SettingsItemNew( + title = { Text(text = stringResource(id = R.string.general_settings)) }, + description = { Text(text = stringResource(id = R.string.general_settings_desc)) }, + icon = Icons.Filled.SettingsApplications, + onClick = { navController.navigate(Route.GENERAL_DOWNLOAD_PREFERENCES) { launchSingleTop = true } - } + }, + addTonalElevation = true, + modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)) + ) } item { - SettingItem( - title = stringResource(id = R.string.spotify_settings), - description = stringResource( - id = R.string.spotify_settings_desc - ), - icon = LocalAsset(id = R.drawable.spotify_logo) - ) { - navController.navigate(Route.SPOTIFY_PREFERENCES) { - launchSingleTop = true - } - } + SettingsItemNew( + title = { Text(text = stringResource(id = R.string.spotify_settings)) }, + description = { Text(text = stringResource(id = R.string.spotify_settings_desc)) }, + icon = LocalAsset(id = R.drawable.spotify_logo), + onClick = { + navController.navigate(Route.SPOTIFY_PREFERENCES) { + launchSingleTop = true + } + }, + addTonalElevation = true + ) } item { - SettingItem( - title = stringResource(id = R.string.download_directory), - description = stringResource( - id = R.string.download_directory_desc - ), - icon = Icons.Filled.Folder - ) { - navController.navigate(Route.DOWNLOAD_DIRECTORY) { - launchSingleTop = true - } - } + //new settings item for download directory + SettingsItemNew( + title = { Text(text = stringResource(id = R.string.download_directory)) }, + description = { Text(text = stringResource(id = R.string.download_directory_desc)) }, + icon = Icons.Filled.Folder, + onClick = { + navController.navigate(Route.DOWNLOAD_DIRECTORY) { + launchSingleTop = true + } + }, + addTonalElevation = true + ) } item { - SettingItem( - title = stringResource(id = R.string.format), - description = stringResource(id = R.string.format_settings_desc), - icon = Icons.Filled.AudioFile - ) { - navController.navigate(Route.DOWNLOAD_FORMAT) { - launchSingleTop = true - } - } + SettingsItemNew( + title = { Text(text = stringResource(id = R.string.format)) }, + description = { Text(text = stringResource(id = R.string.format_settings_desc)) }, + icon = Icons.Filled.AudioFile, + onClick = { + navController.navigate(Route.DOWNLOAD_FORMAT) { + launchSingleTop = true + } + }, + addTonalElevation = true + ) } - /*item { - SettingItem( - title = stringResource(id = R.string.network), - description = stringResource(id = R.string.network_settings_desc), - icon = if (App.connectivityManager.isActiveNetworkMetered) Icons.Filled.SignalCellular4Bar else Icons.Filled.SignalWifi4Bar - ) { - navController.navigate(Route.NETWORK_PREFERENCES) { - launchSingleTop = true - } - } - }*/ item { - SettingItem( - title = stringResource(id = R.string.appearance), description = stringResource( - id = R.string.appearance_settings - ), icon = Icons.Filled.Aod - ) { - navController.navigate(Route.APPEARANCE) { launchSingleTop = true } - } + //rewrite this with new settings item + SettingsItemNew( + title = { Text(text = stringResource(id = R.string.appearance)) }, + description = { Text(text = stringResource(id = R.string.appearance_settings)) }, + icon = Icons.Filled.Aod, + onClick = { + navController.navigate(Route.APPEARANCE) { + launchSingleTop = true + } + }, + addTonalElevation = true + ) } item { //Cookies page - SettingItem( - title = stringResource(id = R.string.cookies), description = stringResource( - id = R.string.cookies_desc - ), icon = Icons.Outlined.Cookie - ) { - navController.navigate(Route.COOKIE_PROFILE) { launchSingleTop = true } - } + SettingsItemNew( + title = { Text(text = stringResource(id = R.string.cookies)) }, + description = { Text(text = stringResource(id = R.string.cookies_desc)) }, + icon = Icons.Outlined.Cookie, + onClick = { + navController.navigate(Route.COOKIE_PROFILE) { + launchSingleTop = true + } + }, + addTonalElevation = true + ) } item { - SettingItem(title = stringResource(id = R.string.documentation), description = stringResource( - id = R.string.documentation_desc - ), icon = Icons.Filled.Help ) { - navController.navigate(Route.DOCUMENTATION) { launchSingleTop = true } - } + SettingsItemNew( + title = { Text(text = stringResource(id = R.string.documentation)) }, + description = { Text(text = stringResource(id = R.string.documentation_desc)) }, + icon = Icons.Filled.Help, + onClick = { + navController.navigate(Route.DOCUMENTATION) { + launchSingleTop = true + } + }, + addTonalElevation = true + ) } item{ - SettingItem( - title = stringResource(id = R.string.updates_channels), description = stringResource( - id = R.string.updates_channels_desc - ), icon = Icons.Filled.Update - ) { - navController.navigate(Route.UPDATER_PAGE) { launchSingleTop = true } - } + SettingsItemNew( + title = { Text(text = stringResource(id = R.string.updates_channels)) }, + description = { Text(text = stringResource(id = R.string.updates_channels_desc)) }, + icon = Icons.Filled.Update, + onClick = { + navController.navigate(Route.UPDATER_PAGE) { + launchSingleTop = true + } + }, + addTonalElevation = true + ) } item { - SettingItem( - title = stringResource(id = R.string.about), description = stringResource( - id = R.string.about_page - ), icon = Icons.Filled.Info - ) { - navController.navigate(Route.ABOUT) { launchSingleTop = true } - } + SettingsItemNew( + title = { Text(text = stringResource(id = R.string.about)) }, + description = { Text(text = stringResource(id = R.string.about_page)) }, + icon = Icons.Filled.Info, + onClick = { + navController.navigate(Route.ABOUT) { + launchSingleTop = true + } + }, + addTonalElevation = true, + modifier = Modifier.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)) + ) + } + item { + Spacer(modifier = Modifier.height(24.dp)) } } } From 563aebb6c704f75cb9dcf52d15cf3a8865f193bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 25 Mar 2023 10:30:16 +0100 Subject: [PATCH 22/42] deps: Moved from deprecated Pager from Accompanist --- app/build.gradle.kts | 17 +-- .../bottomsheets/DownloaderBottomSheet.kt | 143 ++++++++++++++++++ .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 7 +- .../spowlo/ui/pages/settings/SettingsPage.kt | 23 +-- .../settings/appearance/AppearancePage.kt | 8 +- .../com/bobbyesp/spowlo/utils/TextUtils.kt | 10 +- gradle/libs.versions.toml | 1 - 7 files changed, 175 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eaa62cc7..1c63b3f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,8 +43,8 @@ sealed class Version( val currentVersion: Version = Version.Stable( versionMajor = 1, - versionMinor = 2, - versionPatch = 1, + versionMinor = 3, + versionPatch = 0, ) val keystorePropertiesFile = rootProject.file("keystore.properties") @@ -207,10 +207,9 @@ dependencies { implementation(libs.accompanist.permissions) implementation(libs.accompanist.navigation.animation) implementation(libs.accompanist.webview) - implementation(libs.accompanist.pager.layouts) - implementation(libs.accompanist.pager.indicators) implementation(libs.accompanist.flowlayout) implementation(libs.accompanist.material) + implementation(libs.accompanist.pager.indicators) implementation(libs.paging.compose) implementation(libs.paging.runtime) @@ -240,11 +239,11 @@ dependencies { implementation(libs.markdown) //Exoplayer - implementation(libs.exoplayer.core) - implementation(libs.exoplayer.ui) - implementation(libs.exoplayer.dash) - implementation(libs.exoplayer.smoothstreaming) - implementation(libs.exoplayer.extension.mediasession) +// implementation(libs.exoplayer.core) +// implementation(libs.exoplayer.ui) +// implementation(libs.exoplayer.dash) +// implementation(libs.exoplayer.smoothstreaming) +// implementation(libs.exoplayer.extension.mediasession) implementation(libs.customtabs) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt new file mode 100644 index 00000000..f7e4328a --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt @@ -0,0 +1,143 @@ +package com.bobbyesp.spowlo.ui.dialogs.bottomsheets + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabPosition +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.layout +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DownloaderBottomSheet( + onBackPressed : () -> Unit, + downloaderViewModel: DownloaderViewModel +){ + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState(initialPage = 0) + val pages = listOf(BottomSheetPages.MAIN, BottomSheetPages.SECONDARY, BottomSheetPages.TERTIARY) + val viewState by downloaderViewModel.viewStateFlow.collectAsStateWithLifecycle() + val roundedTopShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 0.dp, bottomEnd = 0.dp) + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .navigationBarsPadding() + .clip(roundedTopShape) + + ) { + ScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + indicator = { tabPositions -> + Modifier.pagerTabIndicatorOffset( + pagerState = pagerState, + tabPositions = tabPositions, + pageIndexMapping = { it } + ) + }, + ) { + pages.forEachIndexed { index, title -> + Tab( + text = { Text(title) }, + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(page = index) + } + } + ) + + } + } + /* ScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + indicator = { tabPositions -> + Modifier.pagerTabIndicatorOffset( + pagerState = pagerState, + tabPositions = tabPositions, + pageIndexMapping = { it } + ) + }, + tabs = { + pages.forEachIndexed { index, title -> + Tab( + text = { Text(title) }, + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(page = index) + } + } + ) + + } + } + )*/ + } +} + +object BottomSheetPages { + const val MAIN = "main" + const val SECONDARY = "secondary" + const val TERTIARY = "tertiary" +} + +@OptIn(ExperimentalFoundationApi::class) +fun Modifier.pagerTabIndicatorOffset( + pagerState: PagerState, + tabPositions: List, + pageIndexMapping : (Int) -> Int = { it }, +): Modifier = layout {measurable, constraints -> + if (tabPositions.isEmpty()) { + return@layout layout(0, 0) {} + } else { + val currentPage = minOf(tabPositions.lastIndex, pageIndexMapping(pagerState.currentPage)) + val currentTab = tabPositions[currentPage] + val previousTab = tabPositions.getOrNull(currentPage - 1) + val nextTab = tabPositions.getOrNull(currentPage + 1) + val fraction = pagerState.currentPageOffsetFraction + val indicatorWidth = if (fraction > 0 && nextTab != null) { + lerp(currentTab.width, nextTab.width, fraction).roundToPx() + } else if (fraction < 0 && previousTab != null) { + lerp(currentTab.width, previousTab.width, -fraction).roundToPx() + } else { + currentTab.width.roundToPx() + } + val indicatorOffset = if (fraction > 0 && nextTab != null) { + lerp(currentTab.left, nextTab.left, fraction).roundToPx() + } else if (fraction < 0 && previousTab != null) { + lerp(currentTab.left, previousTab.left, -fraction).roundToPx() + } else { + currentTab.left.roundToPx() + } + val placeable = measurable.measure( + constraints.copy( + minWidth = indicatorWidth, + maxWidth = indicatorWidth, + minHeight = 0, + maxHeight = constraints.maxHeight + ) + ) + layout(constraints.maxWidth, maxOf(placeable.height, constraints.minHeight)) { + placeable.placeRelative(indicatorOffset, maxOf(constraints.minHeight - placeable.height, 0)) + } + } +} diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 8cdd08c2..ff09be70 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -64,6 +64,7 @@ import com.bobbyesp.spowlo.ui.common.animatedComposable import com.bobbyesp.spowlo.ui.common.animatedComposableVariant import com.bobbyesp.spowlo.ui.common.slideInVerticallyComposable import com.bobbyesp.spowlo.ui.dialogs.UpdaterBottomDrawer +import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.DownloaderBottomSheet import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.MoreOptionsHomeBottomSheet import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderPage import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel @@ -240,7 +241,6 @@ fun InitialEntry( text = text, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(top = 6.dp) ) }) } @@ -267,7 +267,6 @@ fun InitialEntry( route = Route.NavGraph ) { navigation(startDestination = Route.HOME, route = Route.DownloaderNavi) { - //TODO: Add all routes animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME DownloaderPage( navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY) }, @@ -409,6 +408,10 @@ fun InitialEntry( } bottomSheet(Route.DOWNLOADER_SHEET) { + DownloaderBottomSheet( + onBackPressed, + downloaderViewModel + ) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt index 9e3aec74..25cb6f1d 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt @@ -22,12 +22,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Aod import androidx.compose.material.icons.filled.AudioFile +import androidx.compose.material.icons.filled.Cookie import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Help import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.SettingsApplications import androidx.compose.material.icons.filled.Update -import androidx.compose.material.icons.outlined.Cookie import androidx.compose.material.icons.rounded.EnergySavingsLeaf import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold @@ -43,6 +43,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.bobbyesp.spowlo.R @@ -123,7 +124,7 @@ fun SettingsPage(navController: NavController) { } item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.general_settings)) }, + title = { Text(text = stringResource(id = R.string.general_settings), fontWeight = FontWeight.Bold) }, description = { Text(text = stringResource(id = R.string.general_settings_desc)) }, icon = Icons.Filled.SettingsApplications, onClick = { @@ -137,7 +138,7 @@ fun SettingsPage(navController: NavController) { } item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.spotify_settings)) }, + title = { Text(text = stringResource(id = R.string.spotify_settings), fontWeight = FontWeight.Bold) }, description = { Text(text = stringResource(id = R.string.spotify_settings_desc)) }, icon = LocalAsset(id = R.drawable.spotify_logo), onClick = { @@ -151,7 +152,7 @@ fun SettingsPage(navController: NavController) { item { //new settings item for download directory SettingsItemNew( - title = { Text(text = stringResource(id = R.string.download_directory)) }, + title = { Text(text = stringResource(id = R.string.download_directory), fontWeight = FontWeight.Bold) }, description = { Text(text = stringResource(id = R.string.download_directory_desc)) }, icon = Icons.Filled.Folder, onClick = { @@ -164,7 +165,7 @@ fun SettingsPage(navController: NavController) { } item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.format)) }, + title = { Text(text = stringResource(id = R.string.format), fontWeight = FontWeight.Bold) }, description = { Text(text = stringResource(id = R.string.format_settings_desc)) }, icon = Icons.Filled.AudioFile, onClick = { @@ -178,7 +179,7 @@ fun SettingsPage(navController: NavController) { item { //rewrite this with new settings item SettingsItemNew( - title = { Text(text = stringResource(id = R.string.appearance)) }, + title = { Text(text = stringResource(id = R.string.appearance), fontWeight = FontWeight.Bold) }, description = { Text(text = stringResource(id = R.string.appearance_settings)) }, icon = Icons.Filled.Aod, onClick = { @@ -192,9 +193,9 @@ fun SettingsPage(navController: NavController) { item { //Cookies page SettingsItemNew( - title = { Text(text = stringResource(id = R.string.cookies)) }, + title = { Text(text = stringResource(id = R.string.cookies), fontWeight = FontWeight.Bold) }, description = { Text(text = stringResource(id = R.string.cookies_desc)) }, - icon = Icons.Outlined.Cookie, + icon = Icons.Filled.Cookie, onClick = { navController.navigate(Route.COOKIE_PROFILE) { launchSingleTop = true @@ -206,7 +207,7 @@ fun SettingsPage(navController: NavController) { item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.documentation)) }, + title = { Text(text = stringResource(id = R.string.documentation), fontWeight = FontWeight.Bold) }, description = { Text(text = stringResource(id = R.string.documentation_desc)) }, icon = Icons.Filled.Help, onClick = { @@ -220,7 +221,7 @@ fun SettingsPage(navController: NavController) { item{ SettingsItemNew( - title = { Text(text = stringResource(id = R.string.updates_channels)) }, + title = { Text(text = stringResource(id = R.string.updates_channels), fontWeight = FontWeight.Bold) }, description = { Text(text = stringResource(id = R.string.updates_channels_desc)) }, icon = Icons.Filled.Update, onClick = { @@ -234,7 +235,7 @@ fun SettingsPage(navController: NavController) { item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.about)) }, + title = { Text(text = stringResource(id = R.string.about), fontWeight = FontWeight.Bold) }, description = { Text(text = stringResource(id = R.string.about_page)) }, icon = Icons.Filled.Info, onClick = { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt index 252c8772..53e91afb 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt @@ -14,6 +14,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -74,9 +76,7 @@ import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.getLanguageDesc import com.bobbyesp.spowlo.utils.palettesMap import com.google.accompanist.pager.ExperimentalPagerApi -import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPagerIndicator -import com.google.accompanist.pager.rememberPagerState import com.google.android.material.color.DynamicColors import com.kyant.monet.Hct import com.kyant.monet.LocalTonalPalettes @@ -103,7 +103,7 @@ val colorList = listOf( @OptIn( ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, - ExperimentalPagerApi::class + ExperimentalFoundationApi::class, ExperimentalPagerApi::class ) @Composable fun AppearancePage( @@ -167,7 +167,7 @@ fun AppearancePage( modifier = Modifier .fillMaxWidth() .clearAndSetSemantics { }, state = pagerState, - count = colorList.size, contentPadding = PaddingValues(horizontal = 12.dp) + pageCount = colorList.size, contentPadding = PaddingValues(horizontal = 12.dp) ) { Row() { ColorButtons(colorList[it]) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/TextUtils.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/TextUtils.kt index da649310..ff40868b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/TextUtils.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/TextUtils.kt @@ -2,14 +2,10 @@ package com.bobbyesp.spowlo.utils import android.content.ClipboardManager import android.content.Context -import android.content.res.Resources -import android.util.Log import android.widget.Toast import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.stringResource import androidx.core.text.isDigitsOnly -import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.App.Companion.applicationScope import com.bobbyesp.spowlo.App.Companion.context import com.bobbyesp.spowlo.R @@ -38,10 +34,10 @@ object GeneralTextUtils { fun convertDuration(durationOfSong: Double): String { //First of all the duration comes with this format "146052" but it has to be "146.052" var duration = 0.0 - if (durationOfSong > 100000.0){ - duration = durationOfSong / 1000 + duration = if (durationOfSong > 100000.0){ + durationOfSong / 1000 } else { - duration = durationOfSong + durationOfSong } val hours = (duration / 3600).toInt() val minutes = ((duration % 3600) / 60).toInt() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fa7af987..1fcc77d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,6 @@ navTransitions = "0.11.0-alpha" androidxPaging = "3.1.1" paginationCompose = "1.0.0-alpha18" - androidxLifecycle = "2.6.0" androidxNavigation = "2.5.3" androidxComposeMaterial3 = "1.1.0-alpha08" From f1439ca95e30c9f573d2f51e9afffcf6c8d64951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 25 Mar 2023 12:12:38 +0100 Subject: [PATCH 23/42] feat: Added tabs to the new bottom drawer --- .../ui/dialogs/DownloaderSettingsDialog.kt | 3 - .../bottomsheets/DownloaderBottomSheet.kt | 205 ++++++++++-------- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 3 +- .../ui/pages/downloader/DownloaderPage.kt | 153 ++++++------- .../settings/appearance/AppearancePage.kt | 172 +++++++-------- .../bobbyesp/spowlo/utils/PreferencesUtil.kt | 5 - 6 files changed, 268 insertions(+), 273 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt index 08888705..9afe3300 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/DownloaderSettingsDialog.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.components.BottomDrawer import com.bobbyesp.spowlo.ui.components.DismissButton @@ -52,7 +51,6 @@ import com.bobbyesp.spowlo.utils.DONT_FILTER_RESULTS import com.bobbyesp.spowlo.utils.GEO_BYPASS import com.bobbyesp.spowlo.utils.ORIGINAL_AUDIO import com.bobbyesp.spowlo.utils.PreferencesUtil -import com.bobbyesp.spowlo.utils.PreferencesUtil.templateStateFlow import com.bobbyesp.spowlo.utils.SKIP_INFO_FETCH import com.bobbyesp.spowlo.utils.SYNCED_LYRICS import com.bobbyesp.spowlo.utils.USE_CACHING @@ -143,7 +141,6 @@ fun DownloaderSettingsDialog( var showClientIdDialog by remember { mutableStateOf(false) } var showClientSecretDialog by remember { mutableStateOf(false) } - val templateList by templateStateFlow.collectAsStateWithLifecycle(ArrayList()) val scrollState = rememberLazyListState() val scope = rememberCoroutineScope() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt index f7e4328a..d1880f74 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt @@ -1,41 +1,94 @@ package com.bobbyesp.spowlo.ui.dialogs.bottomsheets +import android.Manifest +import android.os.Build import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.outlined.Dataset +import androidx.compose.material.icons.outlined.DownloadDone import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.Tab -import androidx.compose.material3.TabPosition +import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.layout +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.lerp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.components.FilledButtonWithIcon +import com.bobbyesp.spowlo.ui.components.OutlinedButtonWithIcon import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel +import com.bobbyesp.spowlo.utils.ToastUtil +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.rememberPermissionState import kotlinx.coroutines.launch -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalPermissionsApi::class) @Composable fun DownloaderBottomSheet( - onBackPressed : () -> Unit, - downloaderViewModel: DownloaderViewModel -){ + onBackPressed: () -> Unit, + downloaderViewModel: DownloaderViewModel, + navController: NavController +) { val scope = rememberCoroutineScope() val pagerState = rememberPagerState(initialPage = 0) + val pages = listOf(BottomSheetPages.MAIN, BottomSheetPages.SECONDARY, BottomSheetPages.TERTIARY) + var selectedTabIndex by remember { mutableStateOf(0) } val viewState by downloaderViewModel.viewStateFlow.collectAsStateWithLifecycle() - val roundedTopShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 0.dp, bottomEnd = 0.dp) + val roundedTopShape = + RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 0.dp, bottomEnd = 0.dp) + + val storagePermission = rememberPermissionState( + permission = Manifest.permission.WRITE_EXTERNAL_STORAGE + ) { b: Boolean -> + if (b) { + downloaderViewModel.startDownloadSong() + } else { + ToastUtil.makeToast(R.string.permission_denied) + } + } + + val checkPermissionOrDownload = { + if (Build.VERSION.SDK_INT > 29 || storagePermission.status == PermissionStatus.Granted) + downloaderViewModel.startDownloadSong() + else { + storagePermission.launchPermissionRequest() + } + } + + val downloadButtonCallback = { + navController.popBackStack() + checkPermissionOrDownload() + } + + val requestMetadata = { + navController.popBackStack() + downloaderViewModel.requestMetadata() + } + Column( modifier = Modifier .fillMaxWidth() @@ -44,53 +97,71 @@ fun DownloaderBottomSheet( .clip(roundedTopShape) ) { - ScrollableTabRow( - selectedTabIndex = pagerState.currentPage, - indicator = { tabPositions -> - Modifier.pagerTabIndicatorOffset( - pagerState = pagerState, - tabPositions = tabPositions, - pageIndexMapping = { it } - ) - }, - ) { - pages.forEachIndexed { index, title -> + //I want to create a pager here with tabs at the top for each page + TabRow(selectedTabIndex = pagerState.currentPage) { + pages.forEachIndexed { index, page -> Tab( - text = { Text(title) }, + text = { Text(text = page) }, selected = pagerState.currentPage == index, onClick = { scope.launch { - pagerState.animateScrollToPage(page = index) + pagerState.animateScrollToPage(index) } } ) - } } - /* ScrollableTabRow( - selectedTabIndex = pagerState.currentPage, - indicator = { tabPositions -> - Modifier.pagerTabIndicatorOffset( - pagerState = pagerState, - tabPositions = tabPositions, - pageIndexMapping = { it } - ) - }, - tabs = { - pages.forEachIndexed { index, title -> - Tab( - text = { Text(title) }, - selected = pagerState.currentPage == index, - onClick = { - scope.launch { - pagerState.animateScrollToPage(page = index) - } - } - ) - + HorizontalPager(pageCount = pages.size, state = pagerState) { + when (pages[it]) { + BottomSheetPages.MAIN -> { + Text(text = "Main page") + } + BottomSheetPages.SECONDARY -> { + Text(text = "Secondary page") + } + BottomSheetPages.TERTIARY -> { + Text(text = "Tertiary page") } } - )*/ + } + + val state = rememberLazyListState() + + LaunchedEffect(Unit) { + state.scrollToItem(1) + } + + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp), + horizontalArrangement = Arrangement.End, + state = state + ) { + item { + OutlinedButtonWithIcon( + modifier = Modifier.padding(horizontal = 12.dp), + onClick = { navController.popBackStack() }, + icon = Icons.Outlined.Cancel, + text = stringResource(R.string.cancel) + ) + } + item { + FilledButtonWithIcon( + modifier = Modifier.padding(end = 12.dp), + onClick = requestMetadata, + icon = Icons.Outlined.Dataset, + text = stringResource(R.string.request_metadata) + ) + } + item { + FilledButtonWithIcon( + onClick = downloadButtonCallback, + icon = Icons.Outlined.DownloadDone, + text = stringResource(R.string.start_download) + ) + } + } } } @@ -99,45 +170,3 @@ object BottomSheetPages { const val SECONDARY = "secondary" const val TERTIARY = "tertiary" } - -@OptIn(ExperimentalFoundationApi::class) -fun Modifier.pagerTabIndicatorOffset( - pagerState: PagerState, - tabPositions: List, - pageIndexMapping : (Int) -> Int = { it }, -): Modifier = layout {measurable, constraints -> - if (tabPositions.isEmpty()) { - return@layout layout(0, 0) {} - } else { - val currentPage = minOf(tabPositions.lastIndex, pageIndexMapping(pagerState.currentPage)) - val currentTab = tabPositions[currentPage] - val previousTab = tabPositions.getOrNull(currentPage - 1) - val nextTab = tabPositions.getOrNull(currentPage + 1) - val fraction = pagerState.currentPageOffsetFraction - val indicatorWidth = if (fraction > 0 && nextTab != null) { - lerp(currentTab.width, nextTab.width, fraction).roundToPx() - } else if (fraction < 0 && previousTab != null) { - lerp(currentTab.width, previousTab.width, -fraction).roundToPx() - } else { - currentTab.width.roundToPx() - } - val indicatorOffset = if (fraction > 0 && nextTab != null) { - lerp(currentTab.left, nextTab.left, fraction).roundToPx() - } else if (fraction < 0 && previousTab != null) { - lerp(currentTab.left, previousTab.left, -fraction).roundToPx() - } else { - currentTab.left.roundToPx() - } - val placeable = measurable.measure( - constraints.copy( - minWidth = indicatorWidth, - maxWidth = indicatorWidth, - minHeight = 0, - maxHeight = constraints.maxHeight - ) - ) - layout(constraints.maxWidth, maxOf(placeable.height, constraints.minHeight)) { - placeable.placeRelative(indicatorOffset, maxOf(constraints.minHeight - placeable.height, 0)) - } - } -} diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index ff09be70..5b520552 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -410,7 +410,8 @@ fun InitialEntry( bottomSheet(Route.DOWNLOADER_SHEET) { DownloaderBottomSheet( onBackPressed, - downloaderViewModel + downloaderViewModel, + navController ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index 2ad5ca3a..15dbf414 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -37,7 +37,7 @@ import androidx.compose.material.icons.outlined.FormatListBulleted import androidx.compose.material.icons.outlined.Subscriptions import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator @@ -133,8 +133,7 @@ fun DownloaderPage( val keyboardController = LocalSoftwareKeyboardController.current val checkPermissionOrDownload = { - if (Build.VERSION.SDK_INT > 29 || storagePermission.status == PermissionStatus.Granted) - downloaderViewModel.startDownloadSong() + if (Build.VERSION.SDK_INT > 29 || storagePermission.status == PermissionStatus.Granted) downloaderViewModel.startDownloadSong() else { storagePermission.launchPermissionRequest() } @@ -171,8 +170,7 @@ fun DownloaderPage( .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { - DownloaderPageImplementation( - downloaderState = downloaderState, + DownloaderPageImplementation(downloaderState = downloaderState, taskState = taskState, viewState = viewState, errorState = errorState, @@ -192,8 +190,7 @@ fun DownloaderPage( matchUrlFromClipboard( string = clipboardManager.getText().toString(), isMatchingMultiLink = CUSTOM_COMMAND.getBoolean() - ) - .let { downloaderViewModel.updateUrl(it) } + ).let { downloaderViewModel.updateUrl(it) } }, cancelCallback = { Downloader.cancelDownload() @@ -201,14 +198,12 @@ fun DownloaderPage( onUrlChanged = { url -> downloaderViewModel.updateUrl(url) }) {} with(viewState) { - DownloaderSettingsDialog( - useDialog = useDialog, + DownloaderSettingsDialog(useDialog = useDialog, dialogState = showDownloadSettingDialog, drawerState = drawerState, confirm = { checkPermissionOrDownload() }, onRequestMetadata = { downloaderViewModel.requestMetadata() }, - hide = { downloaderViewModel.hideDialog(scope, useDialog) } - ) + hide = { downloaderViewModel.hideDialog(scope, useDialog) }) } } } @@ -237,41 +232,38 @@ fun DownloaderPageImplementation( isPreview: Boolean = false, content: @Composable () -> Unit ) { - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar(title = {}, modifier = Modifier.padding(horizontal = 8.dp), - navigationIcon = { - IconButton(onClick = { navigateToSettings() }) { - Icon( - imageVector = Icons.Outlined.FormatListBulleted, - contentDescription = stringResource(id = R.string.show_more_actions) - ) - } - }, actions = { - IconButton(onClick = { navigateToMods() }) { - Icon( - imageVector = LocalAsset(id = R.drawable.spotify_logo), - contentDescription = stringResource(id = R.string.mods_downloader) - ) - } + Scaffold(modifier = Modifier.fillMaxSize(), topBar = { + TopAppBar(title = {}, modifier = Modifier.padding(horizontal = 8.dp), navigationIcon = { + IconButton(onClick = { navigateToSettings() }) { + Icon( + imageVector = Icons.Outlined.FormatListBulleted, + contentDescription = stringResource(id = R.string.show_more_actions) + ) + } + }, actions = { + IconButton(onClick = { navigateToMods() }) { + Icon( + imageVector = LocalAsset(id = R.drawable.spotify_logo), + contentDescription = stringResource(id = R.string.mods_downloader) + ) + } - IconButton(onClick = { navigateToDownloads() }) { - Icon( - imageVector = Icons.Outlined.Subscriptions, - contentDescription = stringResource(id = R.string.downloads_history) - ) - } - }) - }, floatingActionButton = { - FABs( - modifier = with(Modifier) { if (showDownloadProgress) this else this.imePadding() }, - downloadCallback = downloadCallback, - pasteCallback = pasteCallback, - cancelCallback = cancelCallback, - isDownloading = downloaderState is Downloader.State.DownloadingSong, - ) - }) { + IconButton(onClick = { navigateToDownloads() }) { + Icon( + imageVector = Icons.Outlined.Subscriptions, + contentDescription = stringResource(id = R.string.downloads_history) + ) + } + }) + }, floatingActionButton = { + FABs( + modifier = with(Modifier) { if (showDownloadProgress) this else this.imePadding() }, + downloadCallback = downloadCallback, + pasteCallback = pasteCallback, + cancelCallback = cancelCallback, + isDownloading = downloaderState is Downloader.State.DownloadingSong, + ) + }) { Column( modifier = Modifier .padding(it) @@ -309,12 +301,10 @@ fun DownloaderPageImplementation( ) } AnimatedVisibility( - visible = downloaderState !is Downloader.State.Idle, - modifier = Modifier + visible = downloaderState !is Downloader.State.Idle, modifier = Modifier ) { CircularProgressIndicator( - modifier = Modifier - .size(24.dp), + modifier = Modifier.size(24.dp), strokeWidth = 3.dp, ) } @@ -322,14 +312,12 @@ fun DownloaderPageImplementation( with(taskState) { AnimatedVisibility(visible = showSongCard && showDownloadProgress) { Column(modifier = Modifier.fillMaxWidth()) { - SongCard( - song = info, + SongCard(song = info, progress = progress, modifier = Modifier.padding(top = 16.dp, bottom = 4.dp), isLyrics = hasLyrics, isExplicit = info.explicit, - onClick = { onSongCardClicked() } - ) + onClick = { onSongCardClicked() }) Text( text = stringResource(id = R.string.click_card_metadata), modifier = Modifier @@ -353,8 +341,7 @@ fun DownloaderPageImplementation( visible = progressText.isNotEmpty() && showOutput ) { ConsoleOutputComponent( - consoleOutput = progressText, - modifier = Modifier.padding(top = 10.dp) + consoleOutput = progressText, modifier = Modifier.padding(top = 10.dp) ) } @@ -385,36 +372,38 @@ fun FABs( Column( modifier = modifier.padding(6.dp), horizontalAlignment = Alignment.End ) { - FloatingActionButton( - onClick = pasteCallback, - content = { - Icon( - Icons.Outlined.ContentPaste, contentDescription = stringResource(R.string.paste) - ) - }, - modifier = Modifier.padding(vertical = 12.dp), + ExtendedFloatingActionButton(onClick = pasteCallback, text = { + Text(stringResource(R.string.paste)) + }, icon = { + Icon( + Icons.Outlined.ContentPaste, contentDescription = stringResource(R.string.paste) + ) + }, modifier = Modifier.padding(vertical = 12.dp) + ) + ExtendedFloatingActionButton(onClick = downloadCallback, text = { + Text(stringResource(R.string.download)) + }, icon = { + Icon( + Icons.Outlined.FileDownload, + contentDescription = stringResource(R.string.download) + ) + }, modifier = Modifier.padding(vertical = 12.dp) ) - FloatingActionButton( - onClick = downloadCallback, content = { + + } + + /*AnimatedVisibility(visible = isDownloading) { + ExtendedFloatingActionButton( + text = { Text(stringResource(R.string.cancel)) }, + onClick = cancelCallback, icon = { Icon( - Icons.Outlined.FileDownload, - contentDescription = stringResource(R.string.download) + Icons.Outlined.Cancel, + contentDescription = stringResource(R.string.cancel_download) ) }, modifier = Modifier.padding(vertical = 12.dp) ) + }*/ - /*AnimatedVisibility(visible = isDownloading) { - ExtendedFloatingActionButton( - text = { Text(stringResource(R.string.cancel)) }, - onClick = cancelCallback, icon = { - Icon( - Icons.Outlined.Cancel, - contentDescription = stringResource(R.string.cancel_download) - ) - }, modifier = Modifier.padding(vertical = 12.dp) - ) - }*/ - } } @@ -431,8 +420,7 @@ fun InputUrl( val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val softwareKeyboardController = LocalSoftwareKeyboardController.current - OutlinedTextField( - value = url, + OutlinedTextField(value = url, isError = error, onValueChange = onValueChange, label = { Text(stringResource(R.string.url_query_spotify)) }, @@ -444,7 +432,8 @@ fun InputUrl( maxLines = 3, trailingIcon = { if (url.isNotEmpty()) ClearButton { onValueChange("") } - }, keyboardActions = KeyboardActions(onDone = { + }, + keyboardActions = KeyboardActions(onDone = { softwareKeyboardController?.hide() focusManager.moveFocus(FocusDirection.Down) onDone() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt index 53e91afb..6cbf6b65 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt @@ -102,49 +102,46 @@ val colorList = listOf( ) @OptIn( - ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, - ExperimentalFoundationApi::class, ExperimentalPagerApi::class + ExperimentalMaterial3Api::class, + ExperimentalFoundationApi::class, + ExperimentalFoundationApi::class, + ExperimentalPagerApi::class ) @Composable fun AppearancePage( navController: NavHostController ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - rememberTopAppBarState(), - canScroll = { true } - ) + val scrollBehavior = + TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState(), + canScroll = { true }) var showDarkThemeDialog by remember { mutableStateOf(false) } val darkTheme = LocalDarkTheme.current var darkThemeValue by remember { mutableStateOf(darkTheme.darkThemeValue) } val image by remember { mutableStateOf( listOf( - R.drawable.sample, - R.drawable.sample1, - R.drawable.sample2, - R.drawable.sample3 + R.drawable.sample, R.drawable.sample1, R.drawable.sample2, R.drawable.sample3 ).random() ) } - Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + Scaffold(modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - LargeTopAppBar( - title = { - Text( - modifier = Modifier, - text = stringResource(id = R.string.display), - ) - }, navigationIcon = { - BackButton { - navController.popBackStack() - } - }, scrollBehavior = scrollBehavior + LargeTopAppBar(title = { + Text( + modifier = Modifier, + text = stringResource(id = R.string.display), + ) + }, navigationIcon = { + BackButton { + navController.popBackStack() + } + }, scrollBehavior = scrollBehavior ) - }, content = { + }, + content = { Column( Modifier .padding(it) @@ -160,19 +157,19 @@ fun AppearancePage( ), modifier = Modifier.padding(16.dp) ) val pagerState = - rememberPagerState( - initialPage = colorList.indexOf(Color(LocalSeedColor.current)) - .run { if (equals(-1)) 1 else this }) + rememberPagerState(initialPage = colorList.indexOf(Color(LocalSeedColor.current)) + .run { if (equals(-1)) 1 else this }) HorizontalPager( modifier = Modifier .fillMaxWidth() - .clearAndSetSemantics { }, state = pagerState, - pageCount = colorList.size, contentPadding = PaddingValues(horizontal = 12.dp) + .clearAndSetSemantics { }, + state = pagerState, + pageCount = colorList.size, + contentPadding = PaddingValues(horizontal = 12.dp) ) { - Row() { ColorButtons(colorList[it]) } + Row { ColorButtons(colorList[it]) } } - HorizontalPagerIndicator( - pagerState = pagerState, + HorizontalPagerIndicator(pagerState = pagerState, pageCount = colorList.size, modifier = Modifier .clearAndSetSemantics { } @@ -181,12 +178,10 @@ fun AppearancePage( activeColor = MaterialTheme.colorScheme.primary, inactiveColor = MaterialTheme.colorScheme.outlineVariant, indicatorHeight = 6.dp, - indicatorWidth = 6.dp - ) + indicatorWidth = 6.dp) if (DynamicColors.isDynamicColorAvailable()) { - PreferenceSwitch( - title = stringResource(id = R.string.dynamic_color), + PreferenceSwitch(title = stringResource(id = R.string.dynamic_color), description = stringResource( id = R.string.dynamic_color_desc ), @@ -194,64 +189,61 @@ fun AppearancePage( isChecked = LocalDynamicColorSwitch.current, onClick = { PreferencesUtil.switchDynamicColor() - } - ) + }) } val isDarkTheme = LocalDarkTheme.current.isDarkTheme() - PreferenceSwitchWithDivider( - title = stringResource(id = R.string.dark_theme), + PreferenceSwitchWithDivider(title = stringResource(id = R.string.dark_theme), icon = Icons.Outlined.DarkMode, isChecked = isDarkTheme, description = LocalDarkTheme.current.getDarkThemeDesc(), onChecked = { PreferencesUtil.modifyDarkThemePreference(if (isDarkTheme) OFF else ON) }, - onClick = { navController.navigate(Route.APP_THEME) } - ) + onClick = { navController.navigate(Route.APP_THEME) }) //todo: add the languages page - if (Build.VERSION.SDK_INT >= 24) - PreferenceItem( - title = stringResource(R.string.language), - icon = Icons.Outlined.Language, - description = getLanguageDesc() - ) { navController.navigate(Route.LANGUAGES) } + if (Build.VERSION.SDK_INT >= 24) PreferenceItem( + title = stringResource(R.string.language), + icon = Icons.Outlined.Language, + description = getLanguageDesc() + ) { navController.navigate(Route.LANGUAGES) } } }) - if (showDarkThemeDialog) - AlertDialog(onDismissRequest = { - showDarkThemeDialog = false - darkThemeValue = darkTheme.darkThemeValue - }, confirmButton = { + if (showDarkThemeDialog) AlertDialog(onDismissRequest = { + showDarkThemeDialog = false + darkThemeValue = darkTheme.darkThemeValue + }, + confirmButton = { ConfirmButton { showDarkThemeDialog = false PreferencesUtil.modifyDarkThemePreference(darkThemeValue) } - }, dismissButton = { + }, + dismissButton = { DismissButton { showDarkThemeDialog = false darkThemeValue = darkTheme.darkThemeValue } - }, icon = { Icon(Icons.Outlined.DarkMode, null) }, - title = { Text(stringResource(R.string.dark_theme)) }, text = { - Column { - SingleChoiceItem( - text = stringResource(R.string.follow_system), - selected = darkThemeValue == FOLLOW_SYSTEM - ) { - darkThemeValue = FOLLOW_SYSTEM - } - SingleChoiceItem( - text = stringResource(R.string.on), - selected = darkThemeValue == ON - ) { - darkThemeValue = ON - } - SingleChoiceItem( - text = stringResource(R.string.off), - selected = darkThemeValue == OFF - ) { - darkThemeValue = OFF - } + }, + icon = { Icon(Icons.Outlined.DarkMode, null) }, + title = { Text(stringResource(R.string.dark_theme)) }, + text = { + Column { + SingleChoiceItem( + text = stringResource(R.string.follow_system), + selected = darkThemeValue == FOLLOW_SYSTEM + ) { + darkThemeValue = FOLLOW_SYSTEM + } + SingleChoiceItem( + text = stringResource(R.string.on), selected = darkThemeValue == ON + ) { + darkThemeValue = ON + } + SingleChoiceItem( + text = stringResource(R.string.off), selected = darkThemeValue == OFF + ) { + darkThemeValue = OFF } - }) + } + }) } @Composable @@ -271,11 +263,7 @@ fun RowScope.ColorButton( val tonalPalettes = color.toTonalPalettes(tonalStyle) val isSelect = !LocalDynamicColorSwitch.current && LocalSeedColor.current == color.toArgb() && LocalPaletteStyleIndex.current == index - ColorButtonImpl( - modifier = modifier, - tonalPalettes = tonalPalettes, - isSelected = { isSelect } - ) { + ColorButtonImpl(modifier = modifier, tonalPalettes = tonalPalettes, isSelected = { isSelect }) { PreferencesUtil.switchDynamicColor(enabled = false) PreferencesUtil.modifyThemeSeedColor(color.toArgb(), index) } @@ -309,22 +297,18 @@ fun RowScope.ColorButtonImpl( color = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), onClick = { onClick() }) { Box(Modifier.fillMaxSize()) { - Box( - modifier = modifier - .size(48.dp) - .clip(CircleShape) - .drawBehind { drawCircle(color1) } - .align(Alignment.Center) - ) { + Box(modifier = modifier + .size(48.dp) + .clip(CircleShape) + .drawBehind { drawCircle(color1) } + .align(Alignment.Center)) { Surface( - color = color2, - modifier = Modifier + color = color2, modifier = Modifier .align(Alignment.BottomStart) .size(24.dp) ) {} Surface( - color = color3, - modifier = Modifier + color = color3, modifier = Modifier .align(Alignment.BottomEnd) .size(24.dp) ) {} diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt index 1a24fc43..92720cfc 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt @@ -309,11 +309,6 @@ object PreferencesUtil { fun getCookies(): String = cookiesStateFlow.value - val templateStateFlow: StateFlow> = - DatabaseUtil.getTemplateFlow().distinctUntilChanged().stateIn( - applicationScope, started = SharingStarted.Eagerly, emptyList() - ) - } data class DarkThemePreference( From 9794182657e36ec99e2411da27c06faf4f26a42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sun, 26 Mar 2023 11:37:55 +0200 Subject: [PATCH 24/42] feat: Added info cards to the track page and also the ability to download from that page. --- .../java/com/bobbyesp/spowlo/Downloader.kt | 148 +++++++++++++++++- .../java/com/bobbyesp/spowlo/MainActivity.kt | 25 +-- .../components/settings/SettingsComponents.kt | 21 ++- .../songs/metadata_viewer/ExtraInfoCard.kt | 59 +++++++ .../songs/metadata_viewer/TrackComponent.kt | 105 +++++++++++++ .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 37 +++-- .../ui/pages/downloader/DownloaderPage.kt | 18 +-- .../binders/SpotifyPageBinder.kt | 5 +- .../pages/metadata_viewer/pages/TrackPage.kt | 57 +++++-- .../metadata_viewer/playlists/PlaylistPage.kt | 6 +- .../playlists/PlaylistPageViewModel.kt | 6 + .../spowlo/ui/pages/settings/SettingsPage.kt | 25 ++- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 132 ++++++++++------ app/src/main/res/values/strings.xml | 4 + 14 files changed, 531 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index 4b70bb33..00c63e7f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -1,7 +1,10 @@ package com.bobbyesp.spowlo +import android.content.ClipboardManager import android.util.Log import androidx.annotation.CheckResult +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.ui.text.AnnotatedString import com.bobbyesp.library.SpotDL import com.bobbyesp.library.dto.Song import com.bobbyesp.spowlo.App.Companion.applicationScope @@ -16,6 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.util.UUID object Downloader { @@ -30,6 +34,9 @@ object Downloader { object Idle : State() } + fun makeKey(url: String, randomString: String = UUID.randomUUID().toString()): String = + "${randomString}_$url" + data class ErrorState( val errorReport: String = "", val errorMessageResId: Int = R.string.unknown_error, @@ -38,6 +45,61 @@ object Downloader { errorMessageResId != R.string.unknown_error || errorReport.isNotEmpty() } + data class DownloadTask( + val url: String, + val consoleOutput: String, + val state: State, + val currentLine: String + ) { + fun toKey(extraString: String) = makeKey(url, extraString) + + private fun randomString() = UUID.randomUUID().toString() + sealed class State { + data class Error(val errorReport: String) : State() + object Completed : State() + object Canceled : State() + data class Running(val progress: Float) : State() + } + + override fun hashCode(): Int { + return (this.url + randomString()).hashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DownloadTask + + if (url != other.url) return false + if (consoleOutput != other.consoleOutput) return false + if (state != other.state) return false + if (currentLine != other.currentLine) return false + + return true + } + + fun onCopyLog(clipboardManager: ClipboardManager) { + clipboardManager.setText(AnnotatedString(consoleOutput)) + } + + + fun onRestart() { + applicationScope.launch(Dispatchers.IO) { + getInfoAndDownload(url, skipInfoFetch = true) + } + } + + + fun onCopyError(clipboardManager: ClipboardManager) { + clipboardManager.setText(AnnotatedString(currentLine)) + ToastUtil.makeToast(R.string.error_copied) + } + + } + + //---------------------------- + data class DownloadTaskItem( val info: Song = Song(), val spotifyUrl: String = "", @@ -92,6 +154,71 @@ object Downloader { private val processCount = mutableProcessCount.asStateFlow() + //------------------------------------- + + val mutableTaskList = mutableStateMapOf() + + fun onTaskStarted(url: String, extraKey: String) = + DownloadTask( + url = url, + consoleOutput = "", + state = DownloadTask.State.Running(0f), + currentLine = "" + ).run { + mutableTaskList.put(this.toKey(extraKey), this) + } + + fun updateTaskOutput(url: String, line: String, progress: Float) { + val key = makeKey(url) + val oldValue = mutableTaskList[key] ?: return + val newValue = oldValue.run { + copy( + consoleOutput = consoleOutput + line + "\n", + currentLine = line, + state = DownloadTask.State.Running(progress) + ) + } + mutableTaskList[key] = newValue + } + + fun onTaskEnded( + url: String, + response: String? = null + ) { + val key = makeKey(url) + /*NotificationUtil.finishNotification( + notificationId = key.toNotificationId(), + title = key, + text = context.getString(R.string.status_completed), + )*/ + mutableTaskList.run { + val oldValue = get(key) ?: return + val newValue = oldValue.copy(state = DownloadTask.State.Completed).run { + response?.let { copy(consoleOutput = response) } ?: this + } + this[key] = newValue + } + FilesUtil.scanDownloadDirectoryToMediaLibrary(App.audioDownloadDir) + } + + fun onTaskError(errorReport: String, url: String, extraString: String) = + mutableTaskList.run { + val key = makeKey(url, extraString) + /*NotificationUtil.makeErrorReportNotification( + notificationId = key.toNotificationId(), + error = errorReport + )*/ + val oldValue = mutableTaskList[key] ?: return + mutableTaskList[key] = oldValue.copy( + state = DownloadTask.State.Error( + errorReport + ), + currentLine = errorReport, + consoleOutput = oldValue.consoleOutput + "\n" + errorReport + ) + } + + fun onProcessEnded() = mutableProcessCount.update { it - 1 } @@ -116,8 +243,6 @@ object Downloader { if (!isDownloadingPlaylist) updateState(State.DownloadingSong) return DownloaderUtil.downloadSong( songInfo = songInfo, - playlistUrl = "", - playlistItem = 0, downloadPreferences = preferences, taskId = songInfo.song_id + preferences.hashCode() ) { progress, _, line -> @@ -167,7 +292,7 @@ object Downloader { ) { currentJob = applicationScope.launch(Dispatchers.IO) { updateState(State.FetchingInfo) - if(skipInfoFetch){ + if (skipInfoFetch) { downloadResultTemp = downloadSong( songInfo = Song(url = url), preferences = downloadPreferences @@ -179,8 +304,7 @@ object Downloader { ) } return@launch - } - else { + } else { DownloaderUtil.fetchSongInfoFromUrl( url = url ) @@ -221,7 +345,11 @@ object Downloader { } .onSuccess { info -> DownloaderUtil.updateSongsState(info) - mutableTaskState.update { DownloaderUtil.songsState.value[0].toTask(preferencesHash = downloadPreferences.hashCode()) } + mutableTaskState.update { + DownloaderUtil.songsState.value[0].toTask( + preferencesHash = downloadPreferences.hashCode() + ) + } finishProcessing() } } @@ -303,4 +431,12 @@ object Downloader { } } + fun executeParallelDownloadWithUrl(url: String) = + applicationScope.launch(Dispatchers.IO) { + DownloaderUtil.executeParallelDownload( + url + ) + } + + fun onProcessStarted() = mutableProcessCount.update { it + 1 } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt b/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt index 51708341..05848b03 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt @@ -24,6 +24,7 @@ import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.common.SettingsProvider import com.bobbyesp.spowlo.ui.pages.InitialEntry import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists.PlaylistPageViewModel import com.bobbyesp.spowlo.ui.pages.mod_downloader.ModsDownloaderViewModel import com.bobbyesp.spowlo.ui.theme.SpowloTheme import com.bobbyesp.spowlo.utils.PreferencesUtil @@ -37,6 +38,8 @@ import kotlinx.coroutines.runBlocking class MainActivity : AppCompatActivity() { private val downloaderViewModel: DownloaderViewModel by viewModels() private val modsDownloaderViewModel: ModsDownloaderViewModel by viewModels() + private val playlistPageViewModel: PlaylistPageViewModel by viewModels() + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -46,10 +49,9 @@ class MainActivity : AppCompatActivity() { insets } runBlocking { - if (Build.VERSION.SDK_INT < 33) - AppCompatDelegate.setApplicationLocales( - LocaleListCompat.forLanguageTags(PreferencesUtil.getLanguageConfiguration()) - ) + if (Build.VERSION.SDK_INT < 33) AppCompatDelegate.setApplicationLocales( + LocaleListCompat.forLanguageTags(PreferencesUtil.getLanguageConfiguration()) + ) } context = this.baseContext setContent { @@ -65,6 +67,7 @@ class MainActivity : AppCompatActivity() { InitialEntry( downloaderViewModel = downloaderViewModel, modsDownloaderViewModel = modsDownloaderViewModel, + playlistPageViewModel = playlistPageViewModel, isUrlShared = isUrlSharingTriggered ) } @@ -73,7 +76,8 @@ class MainActivity : AppCompatActivity() { handleShareIntent(intent) } - //This function is very important. It handles the intent of opening the app from a shared link and put it in the url field + //This function is very important. + //It handles the intent of opening the app from a shared link and put it in the url field override fun onNewIntent(intent: Intent) { handleShareIntent(intent) super.onNewIntent(intent) @@ -91,11 +95,9 @@ class MainActivity : AppCompatActivity() { } Intent.ACTION_SEND -> { - intent.getStringExtra(Intent.EXTRA_TEXT) - ?.let { sharedContent -> + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { sharedContent -> intent.removeExtra(Intent.EXTRA_TEXT) - matchUrlFromSharedText(sharedContent) - .let { matchedUrl -> + matchUrlFromSharedText(sharedContent).let { matchedUrl -> if (sharedUrl != matchedUrl) { sharedUrl = matchedUrl downloaderViewModel.updateUrl(sharedUrl, true) @@ -112,9 +114,8 @@ class MainActivity : AppCompatActivity() { fun setLanguage(locale: String) { Log.d(TAG, "setLanguage: $locale") - val localeListCompat = - if (locale.isEmpty()) LocaleListCompat.getEmptyLocaleList() - else LocaleListCompat.forLanguageTags(locale) + val localeListCompat = if (locale.isEmpty()) LocaleListCompat.getEmptyLocaleList() + else LocaleListCompat.forLanguageTags(locale) App.applicationScope.launch(Dispatchers.Main) { AppCompatDelegate.setApplicationLocales(localeListCompat) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt index 802e1ccf..db8cca29 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.Icon import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.runtime.Composable @@ -21,7 +22,8 @@ fun SettingsItemNew( trailing: (@Composable () -> Unit)? = null, icon: ImageVector? = null, addTonalElevation: Boolean = false, - clipCorners: Boolean = false + clipCorners: Boolean = false, + highlightIcon : Boolean = false ) { ListItem( modifier = Modifier @@ -38,7 +40,10 @@ fun SettingsItemNew( trailingContent = trailing, supportingContent = description, headlineContent = title, - tonalElevation = if (addTonalElevation) 3.dp else 0.dp + tonalElevation = if (addTonalElevation) 3.dp else 0.dp, + colors = ListItemDefaults.colors( + leadingIconColor = if(highlightIcon) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + ) ) } @@ -52,7 +57,8 @@ fun SettingsItemNew( trailing: (@Composable () -> Unit)? = null, icon: ImageVector? = null, addTonalElevation: Boolean = false, - clipCorners: Boolean = false + clipCorners: Boolean = false, + highlightIcon : Boolean = false ) { SettingsItemNew( modifier = modifier @@ -66,7 +72,8 @@ fun SettingsItemNew( title = title, trailing = trailing, addTonalElevation = addTonalElevation, - clipCorners = clipCorners + clipCorners = clipCorners, + highlightIcon = highlightIcon ) } @@ -81,7 +88,8 @@ fun SettingsSwitch( icon: ImageVector? = null, thumbContent: (@Composable () -> Unit)? = null, addTonalElevation: Boolean = false, - clipCorners: Boolean = false + clipCorners: Boolean = false, + highlightIcon: Boolean = false ) { val toggleableModifier = if (onCheckedChange != null) { Modifier.toggleable( @@ -106,6 +114,7 @@ fun SettingsSwitch( ) }, addTonalElevation = addTonalElevation, - clipCorners = clipCorners + clipCorners = clipCorners, + highlightIcon = highlightIcon ) } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt new file mode 100644 index 00000000..38bfccb0 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt @@ -0,0 +1,59 @@ +package com.bobbyesp.spowlo.ui.components.songs.metadata_viewer + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +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.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ExtraInfoCard( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + headlineText: String = "POPULARITY", + bodyText: String = "69" +) { + OutlinedCard( + onClick = onClick, + shape = MaterialTheme.shapes.medium, + modifier = modifier.size(width = 175.dp, height = 100.dp), + colors = CardDefaults.outlinedCardColors( + containerColor = Color.Transparent, + ) + ) { + Text( + text = headlineText, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier + .align(alignment = Alignment.CenterHorizontally) + .padding(top = 8.dp) + ) + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = bodyText, + style = MaterialTheme.typography.headlineLarge, + modifier = Modifier, + fontWeight = FontWeight.ExtraBold + ) + } + } +} + +@Preview +@Composable +fun ExtraInfoCardPreview() { + ExtraInfoCard() +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt new file mode 100644 index 00000000..9e6414fc --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt @@ -0,0 +1,105 @@ +package com.bobbyesp.spowlo.ui.components.songs.metadata_viewer + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.bobbyesp.spowlo.ui.components.MarqueeText +import com.bobbyesp.spowlo.ui.components.songs.ExplicitIcon +import com.bobbyesp.spowlo.ui.components.songs.LyricsIcon +import com.bobbyesp.spowlo.utils.ChromeCustomTabsUtil + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TrackComponent( + modifier: Modifier = Modifier, + contentModifier: Modifier = Modifier, + songName: String, + artists: String, + spotifyUrl: String, + hasLyrics: Boolean = false, + isExplicit : Boolean = false, + onClick: () -> Unit = { ChromeCustomTabsUtil.openUrl(spotifyUrl)} +) { + Column( + modifier + .fillMaxWidth() + .clickable { onClick() }) { + Row( + modifier = contentModifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, //This makes all go to the center + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Column( + modifier = Modifier + .padding(6.dp) + .weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + MarqueeText( + text = songName, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + basicGradientColor = MaterialTheme.colorScheme.surface.copy( + alpha = 0.8f + ), + ) + } + Spacer(Modifier.height(6.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ){ + MarqueeText( + text = artists, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 10.sp, + basicGradientColor = MaterialTheme.colorScheme.surface.copy( + alpha = 0.8f + ), + ) + Spacer(modifier = Modifier.width(6.dp)) + LyricsIcon(visible = hasLyrics) + Spacer(modifier = Modifier.width(6.dp)) + ExplicitIcon(visible = isExplicit) + } + } + } + Icon(imageVector = Icons.Filled.Download, contentDescription = "Download icon", modifier = Modifier.weight(0.1f).padding(6.dp)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 5b520552..5a67268f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar @@ -70,6 +69,7 @@ import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderPage import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel import com.bobbyesp.spowlo.ui.pages.history.DownloadsHistoryPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists.PlaylistPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists.PlaylistPageViewModel import com.bobbyesp.spowlo.ui.pages.mod_downloader.ModsDownloaderPage import com.bobbyesp.spowlo.ui.pages.mod_downloader.ModsDownloaderViewModel import com.bobbyesp.spowlo.ui.pages.playlist.PlaylistMetadataPage @@ -106,12 +106,13 @@ private const val TAG = "InitialEntry" @OptIn( ExperimentalAnimationApi::class, ExperimentalMaterialNavigationApi::class, - ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class + ExperimentalLayoutApi::class ) @Composable fun InitialEntry( downloaderViewModel: DownloaderViewModel, modsDownloaderViewModel: ModsDownloaderViewModel, + playlistPageViewModel: PlaylistPageViewModel, isUrlShared: Boolean ) { //bottom sheet remember state @@ -167,13 +168,12 @@ fun InitialEntry( } } } - val cookiesViewModel: CookiesSettingsViewModel = viewModel() val onBackPressed: () -> Unit = { navController.popBackStack() } if (isUrlShared) { - if (navController.currentDestination?.route != Route.HOME) { - navController.popBackStack(route = Route.HOME, inclusive = false, saveState = true) + if (navController.currentDestination?.route != Route.DOWNLOADER) { + navController.popBackStack(route = Route.DOWNLOADER, inclusive = false, saveState = true) } } Box( @@ -266,17 +266,26 @@ fun InitialEntry( startDestination = Route.DownloaderNavi, route = Route.NavGraph ) { - navigation(startDestination = Route.HOME, route = Route.DownloaderNavi) { - animatedComposable(Route.HOME) { //TODO: Change this route to Route.DOWNLOADER, but by now, keep it as Route.HOME + navigation(startDestination = Route.DOWNLOADER, route = Route.DownloaderNavi) { + animatedComposable(Route.DOWNLOADER) { DownloaderPage( - navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY) }, - navigateToSettings = { navController.navigate(Route.MORE_OPTIONS_HOME) }, - navigateToDownloaderSheet = { navController.navigate(Route.DOWNLOADER_SHEET) }, + navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY){ + launchSingleTop = true + } }, + navigateToSettings = { navController.navigate(Route.MORE_OPTIONS_HOME) { + launchSingleTop = true + }}, + navigateToDownloaderSheet = { navController.navigate(Route.DOWNLOADER_SHEET){ + launchSingleTop = true + } }, onSongCardClicked = { - navController.navigate(Route.PLAYLIST_METADATA_PAGE) + navController.navigate(Route.PLAYLIST_METADATA_PAGE){ + launchSingleTop = true + } }, - onNavigateToTaskList = { navController.navigate(Route.TASK_LIST) }, - navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) }, + navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) { + launchSingleTop = true + }}, downloaderViewModel = downloaderViewModel ) } @@ -460,7 +469,7 @@ fun InitialEntry( PlaylistPage( onBackPressed, id = id, - type = type + type = type, ) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index 15dbf414..89cfb616 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -30,11 +30,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.ContentAlpha import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FormatListBulleted +import androidx.compose.material.icons.filled.LibraryMusic import androidx.compose.material.icons.outlined.ContentPaste import androidx.compose.material.icons.outlined.Error import androidx.compose.material.icons.outlined.FileDownload -import androidx.compose.material.icons.outlined.FormatListBulleted -import androidx.compose.material.icons.outlined.Subscriptions import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton @@ -106,7 +106,6 @@ fun DownloaderPage( navigateToDownloads: () -> Unit = {}, navigateToDownloaderSheet: () -> Unit = {}, onSongCardClicked: () -> Unit = {}, - onNavigateToTaskList: () -> Unit = {}, navigateToMods: () -> Unit = {}, downloaderViewModel: DownloaderViewModel = hiltViewModel(), ) { @@ -181,7 +180,6 @@ fun DownloaderPage( }, navigateToDownloads = navigateToDownloads, navigateToMods = navigateToMods, - onNavigateToTaskList = onNavigateToTaskList, onSongCardClicked = { songCardClicked() }, showOutput = showConsoleOutput, showSongCard = true, @@ -224,7 +222,6 @@ fun DownloaderPageImplementation( navigateToSettings: () -> Unit = {}, navigateToDownloads: () -> Unit = {}, navigateToMods: () -> Unit = {}, - onNavigateToTaskList: () -> Unit = {}, pasteCallback: () -> Unit = {}, cancelCallback: () -> Unit = {}, onSongCardClicked: () -> Unit = {}, @@ -236,7 +233,7 @@ fun DownloaderPageImplementation( TopAppBar(title = {}, modifier = Modifier.padding(horizontal = 8.dp), navigationIcon = { IconButton(onClick = { navigateToSettings() }) { Icon( - imageVector = Icons.Outlined.FormatListBulleted, + imageVector = Icons.Filled.FormatListBulleted, contentDescription = stringResource(id = R.string.show_more_actions) ) } @@ -250,7 +247,7 @@ fun DownloaderPageImplementation( IconButton(onClick = { navigateToDownloads() }) { Icon( - imageVector = Icons.Outlined.Subscriptions, + imageVector = Icons.Filled.LibraryMusic, contentDescription = stringResource(id = R.string.downloads_history) ) } @@ -390,7 +387,7 @@ fun FABs( }, modifier = Modifier.padding(vertical = 12.dp) ) - } + } /*AnimatedVisibility(visible = isDownloading) { ExtendedFloatingActionButton( @@ -403,8 +400,6 @@ fun FABs( }, modifier = Modifier.padding(vertical = 12.dp) ) }*/ - - } @OptIn(ExperimentalComposeUiApi::class) @@ -420,7 +415,8 @@ fun InputUrl( val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val softwareKeyboardController = LocalSoftwareKeyboardController.current - OutlinedTextField(value = url, + OutlinedTextField( + value = url, isError = error, onValueChange = onValueChange, label = { Text(stringResource(R.string.url_query_spotify)) }, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt index 97123004..ab6c318c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt @@ -17,7 +17,8 @@ import com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages.TrackPage fun SpotifyPageBinder( data: Any, type: SpotifyDataType, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + trackDownloadCallback : (String) -> Unit, ) { Column(modifier = modifier) { when (type) { @@ -45,7 +46,7 @@ fun SpotifyPageBinder( SpotifyDataType.TRACK -> { val track = data as? Track track?.let { - TrackPage(track, modifier) + TrackPage(track, modifier, trackDownloadCallback) } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index 94f81dde..1c56551b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -2,12 +2,15 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -24,26 +27,28 @@ import androidx.compose.ui.unit.dp import com.adamratzman.spotify.models.Track import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.AsyncImageImpl +import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.MarqueeText +import com.bobbyesp.spowlo.ui.components.songs.metadata_viewer.ExtraInfoCard +import com.bobbyesp.spowlo.ui.components.songs.metadata_viewer.TrackComponent import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.dataStringToString +import com.bobbyesp.spowlo.utils.GeneralTextUtils @Composable fun TrackPage( data: Track, - modifier: Modifier + modifier: Modifier, + trackDownloadCallback: (String) -> Unit, ) { val localConfig = LocalConfiguration.current Column( - modifier = modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 12.dp) + modifier = modifier.fillMaxSize() ) { Box( modifier = Modifier .clip(MaterialTheme.shapes.extraSmall) .fillMaxWidth() - .padding(top = 6.dp, bottom = 6.dp), - contentAlignment = Alignment.Center + .padding(top = 16.dp, bottom = 6.dp), contentAlignment = Alignment.Center ) { //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height val size = (localConfig.screenHeightDp / 3) @@ -51,8 +56,7 @@ fun TrackPage( modifier = Modifier .size(size.dp) .aspectRatio( - 1f, - matchHeightConstraintsFirst = true + 1f, matchHeightConstraintsFirst = true ) .clip(MaterialTheme.shapes.small), model = data.album.images[0].url, @@ -63,6 +67,7 @@ fun TrackPage( Column( modifier = Modifier .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 8.dp) ) { SelectionContainer { MarqueeText( @@ -85,12 +90,44 @@ fun TrackPage( SelectionContainer { Text( text = dataStringToString( - data = data.type, - additional = data.album.releaseDate!!.year.toString()), //TODO: CHANGE THIS TO NOT BE HARDCODED + data = data.type, additional = data.album.releaseDate?.year.toString() + ), style = MaterialTheme.typography.bodySmall, modifier = Modifier.alpha(alpha = 0.8f) ) } } + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) + Column( + modifier = Modifier.fillMaxWidth() + ) { + TrackComponent(contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + songName = data.name, + artists = data.artists.joinToString(", ") { it.name }, + spotifyUrl = data.externalUrls.spotify!!, + isExplicit = data.explicit, + onClick = { trackDownloadCallback(data.externalUrls.spotify!!) }) + } + Spacer(modifier = Modifier.padding(vertical = 8.dp)) + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + ExtraInfoCard( + headlineText = stringResource(id = R.string.track_popularity), + bodyText = data.popularity.toString(), + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(16.dp)) + ExtraInfoCard( + headlineText = stringResource(id = R.string.track_duration), + bodyText = GeneralTextUtils.convertDuration(data.durationMs.toDouble()), + modifier = Modifier.weight(1f) + ) + + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt index 6a0353b2..2d83d037 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt @@ -31,7 +31,7 @@ fun PlaylistPage( onBackPressed: () -> Unit, playlistPageViewModel: PlaylistPageViewModel = hiltViewModel(), id: String, - type : String + type : String, ) { val scope = rememberCoroutineScope() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -77,7 +77,9 @@ fun PlaylistPage( .fillMaxSize()) { item{ Box(Modifier.animateItemPlacement()) { - SpotifyPageBinder(data = state.data, type = typeOfSpotifyDataType(type), modifier = Modifier) + SpotifyPageBinder(data = state.data, type = typeOfSpotifyDataType(type), modifier = Modifier, trackDownloadCallback = { url -> + playlistPageViewModel.downloadTrack(url) + } ) } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt index 6681cbe3..5af02f73 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt @@ -1,6 +1,7 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists import androidx.lifecycle.ViewModel +import com.bobbyesp.spowlo.Downloader import com.bobbyesp.spowlo.features.spotify_api.data.remote.SpotifyApiRequests import com.bobbyesp.spowlo.features.spotify_api.model.SpotifyDataType import kotlinx.coroutines.flow.MutableStateFlow @@ -87,6 +88,11 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { } } } + + fun downloadTrack(url: String) { + Downloader.executeParallelDownloadWithUrl(url) + } + } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt index 25cb6f1d..2517b2da 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt @@ -133,7 +133,8 @@ fun SettingsPage(navController: NavController) { } }, addTonalElevation = true, - modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)) + modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)), + highlightIcon = true ) } item { @@ -146,7 +147,8 @@ fun SettingsPage(navController: NavController) { launchSingleTop = true } }, - addTonalElevation = true + addTonalElevation = true, + highlightIcon = true ) } item { @@ -160,7 +162,8 @@ fun SettingsPage(navController: NavController) { launchSingleTop = true } }, - addTonalElevation = true + addTonalElevation = true, + highlightIcon = true ) } item { @@ -173,7 +176,8 @@ fun SettingsPage(navController: NavController) { launchSingleTop = true } }, - addTonalElevation = true + addTonalElevation = true, + highlightIcon = true ) } item { @@ -187,7 +191,8 @@ fun SettingsPage(navController: NavController) { launchSingleTop = true } }, - addTonalElevation = true + addTonalElevation = true, + highlightIcon = true ) } item { @@ -201,7 +206,8 @@ fun SettingsPage(navController: NavController) { launchSingleTop = true } }, - addTonalElevation = true + addTonalElevation = true, + highlightIcon = true ) } @@ -215,7 +221,8 @@ fun SettingsPage(navController: NavController) { launchSingleTop = true } }, - addTonalElevation = true + addTonalElevation = true, + highlightIcon = true ) } @@ -229,7 +236,8 @@ fun SettingsPage(navController: NavController) { launchSingleTop = true } }, - addTonalElevation = true + addTonalElevation = true, + highlightIcon = true ) } @@ -244,6 +252,7 @@ fun SettingsPage(navController: NavController) { } }, addTonalElevation = true, + highlightIcon = true, modifier = Modifier.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)) ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index 5d52ccda..2e96a36f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -9,6 +9,12 @@ import com.bobbyesp.library.SpotDLRequest import com.bobbyesp.library.dto.Song import com.bobbyesp.spowlo.App.Companion.audioDownloadDir import com.bobbyesp.spowlo.App.Companion.context +import com.bobbyesp.spowlo.Downloader +import com.bobbyesp.spowlo.Downloader.onProcessEnded +import com.bobbyesp.spowlo.Downloader.onProcessStarted +import com.bobbyesp.spowlo.Downloader.onTaskEnded +import com.bobbyesp.spowlo.Downloader.onTaskError +import com.bobbyesp.spowlo.Downloader.onTaskStarted import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.database.DownloadedSongInfo import com.bobbyesp.spowlo.ui.pages.settings.cookies.Cookie @@ -24,8 +30,8 @@ import java.util.UUID object DownloaderUtil { - private const val COOKIE_HEADER = "# Netscape HTTP Cookie File\n" + - "# Auto-generated by Spowlo built-in WebView\n" + private const val COOKIE_HEADER = + "# Netscape HTTP Cookie File\n" + "# Auto-generated by Spowlo built-in WebView\n" private const val TAG = "DownloaderUtil" @@ -58,7 +64,7 @@ object DownloaderUtil { val useSyncedLyrics: Boolean = PreferencesUtil.getValue(SYNCED_LYRICS), val useCaching: Boolean = PreferencesUtil.getValue(USE_CACHING), val dontFilter: Boolean = PreferencesUtil.getValue(DONT_FILTER_RESULTS), - val geoBypass : Boolean = PreferencesUtil.getValue(GEO_BYPASS), + val geoBypass: Boolean = PreferencesUtil.getValue(GEO_BYPASS), val formatId: String = "", val privateMode: Boolean = PreferencesUtil.getValue(PRIVATE_MODE), val sdcard: Boolean = PreferencesUtil.getValue(SDCARD_DOWNLOAD), @@ -80,7 +86,8 @@ object DownloaderUtil { flush() } SQLiteDatabase.openDatabase( - "/data/data/com.bobbyesp.spowlo/app_webview/Default/Cookies", null, + "/data/data/com.bobbyesp.spowlo/app_webview/Default/Cookies", + null, SQLiteDatabase.OPEN_READONLY ).run { val projection = arrayOf( @@ -141,22 +148,20 @@ object DownloaderUtil { @CheckResult private fun getSongInfo( url: String? = null, - ): Result> = - kotlin.runCatching { - val response: List = SpotDL.getInstance().getSongInfo(url ?: "") - mutableSongsState.update { - response - } + ): Result> = kotlin.runCatching { + val response: List = SpotDL.getInstance().getSongInfo(url ?: "") + mutableSongsState.update { response } + response + } @CheckResult fun fetchSongInfoFromUrl( url: String - ): Result> = - kotlin.run { - getSongInfo(url) - } + ): Result> = kotlin.run { + getSongInfo(url) + } fun updateSongsState(songs: List) { mutableSongsState.update { @@ -207,28 +212,20 @@ object DownloaderUtil { addOption("--audio", "youtube-music") addOption("youtube") } + 2 -> addOption("--audio", "youtube-music") 3 -> addOption("--audio", "youtube") } } - @CheckResult - fun downloadSong( - songInfo: Song = Song(), - playlistUrl: String = "", - playlistItem: Int = 0, - taskId: String, + //HERE GOES ALL THE DOWNLOADER OPTIONS + private fun commonRequest( downloadPreferences: DownloadPreferences, - progressCallback: ((Float, Long, String) -> Unit)? - ): Result> { - if (songInfo == Song()) return Result.failure(Throwable(context.getString(R.string.fetch_info_error_msg))) + url: String, + request: SpotDLRequest, + pathBuilder: StringBuilder + ): SpotDLRequest { with(downloadPreferences) { - val url = playlistUrl.ifEmpty { - songInfo.url - } - val request = SpotDLRequest() - val pathBuilder = StringBuilder() - request.apply { addOption("download", url) @@ -257,7 +254,7 @@ object DownloaderUtil { addOption("--lyrics", "synced") } - if(geoBypass) { + if (geoBypass) { addOption("--geo-bypass") } @@ -271,6 +268,26 @@ object DownloaderUtil { addAudioProvider() + for (s in request.buildCommand()) Log.d(TAG, s) + } + } + return request + } + + @CheckResult + fun downloadSong( + songInfo: Song = Song(), + taskId: String, + downloadPreferences: DownloadPreferences, + progressCallback: ((Float, Long, String) -> Unit)? + ): Result> { + if (songInfo == Song()) return Result.failure(Throwable(context.getString(R.string.fetch_info_error_msg))) + with(downloadPreferences) { + val url = songInfo.url + + val request = SpotDLRequest() + val pathBuilder = StringBuilder() + commonRequest(downloadPreferences, url, request, pathBuilder).apply { if (useSpotifyPreferences) { if (spotifyClientID.isEmpty() || spotifyClientSecret.isEmpty()) return Result.failure( Throwable("Spotify client ID or secret is empty while you have the custom credentials option enabled! \n Please check your settings.") @@ -278,8 +295,6 @@ object DownloaderUtil { addOption("--client-id", spotifyClientID) addOption("--client-secret", spotifyClientSecret) } - - for (s in request.buildCommand()) Log.d(TAG, s) }.runCatching { SpotDL.getInstance().execute(this, taskId, callback = progressCallback) }.onFailure { th -> @@ -317,21 +332,16 @@ object DownloaderUtil { } private fun onFinishDownloading( - preferences: DownloadPreferences, - songInfo: Song, - downloadPath: String, - sdcardUri: String + preferences: DownloadPreferences, songInfo: Song, downloadPath: String, sdcardUri: String ): Result> = preferences.run { if (privateMode) { Result.success(emptyList()) } else if (sdcard) { - Result.success( - moveFilesToSdcard( - sdcardUri = sdcardUri, - tempPath = context.getSdcardTempDir(songInfo.song_id) - ).apply { - insertInfoIntoDownloadHistory(songInfo, this) - }) + Result.success(moveFilesToSdcard( + sdcardUri = sdcardUri, tempPath = context.getSdcardTempDir(songInfo.song_id) + ).apply { + insertInfoIntoDownloadHistory(songInfo, this) + }) } else { Result.success( scanVideoIntoDownloadHistory( @@ -354,13 +364,12 @@ object DownloaderUtil { } private fun insertInfoIntoDownloadHistory( - songInfo: Song, - filePaths: List + songInfo: Song, filePaths: List ) { filePaths.forEach { filePath -> DatabaseUtil.insertInfo( DownloadedSongInfo( - id = 0, + id = songInfo.name.toInt() + filePath.toInt(), songName = songInfo.name, songAuthor = songInfo.artist, songUrl = songInfo.url, @@ -372,4 +381,35 @@ object DownloaderUtil { ) } } + + + fun executeParallelDownload(url: String) { + val taskId = Downloader.makeKey(url = url, randomString = getRandomUUID()) + ToastUtil.makeToastSuspend(context.getString(R.string.download_started_msg)) + + val pathBuilder = StringBuilder() + val downloadPreferences = DownloadPreferences() + val request = commonRequest(downloadPreferences, url, SpotDLRequest(), pathBuilder) + + onProcessStarted() + onTaskStarted(url, taskId) + kotlin.runCatching { + val response = SpotDL.getInstance().execute(request, taskId) { progress, _, text -> + Downloader.updateTaskOutput( + url = url, line = text, progress = progress + ) + } + onTaskEnded(url, response.output) + }.onFailure { + it.printStackTrace() + ToastUtil.makeToastSuspend(context.getString(R.string.download_error_msg)) + if (it is SpotDL.CanceledException) return@onFailure + it.message.run { + if (isNullOrEmpty()) onTaskEnded(url) + else onTaskError(this, url, taskId) + } + } + onProcessEnded() + ToastUtil.makeToastSuspend(context.getString(R.string.download_finished_msg)) + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e97e8728..a6228bb3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -280,4 +280,8 @@ Go back Both The app updates checker failed + Popularity + Duration + Download started + Download finished \ No newline at end of file From 39b7da7ed26089b34c613f37fafced388e378679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sun, 26 Mar 2023 17:42:20 +0200 Subject: [PATCH 25/42] beaut: Improved the pager of the downloader bottom sheet and added a popup menu in the track page --- .../spowlo/ui/components/songs/SongCard.kt | 4 +- .../songs/metadata_viewer/TrackComponent.kt | 92 ++++++- .../bottomsheets/DownloaderBottomSheet.kt | 72 +++++- .../ui/dialogs/bottomsheets/PagerUtils.kt | 242 ++++++++++++++++++ .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 4 +- .../ui/pages/downloader/DownloaderPage.kt | 11 +- .../mod_downloader/ModsDownloaderPage.kt | 21 +- .../spowlo/ui/pages/searcher/SearcherPage.kt | 12 +- 8 files changed, 417 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/PagerUtils.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt index 0505b67d..aec61a25 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -28,7 +27,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -102,7 +100,7 @@ fun SongCard( ) Spacer(modifier = Modifier.width(6.dp)) LyricsIcon( - visible = isLyrics + visible = false //isLyrics ) } Spacer(Modifier.height(8.dp)) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt index 9e6414fc..cce8182e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt @@ -8,22 +8,38 @@ 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.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Divider +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.PopupProperties +import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.components.MarqueeText import com.bobbyesp.spowlo.ui.components.songs.ExplicitIcon import com.bobbyesp.spowlo.ui.components.songs.LyricsIcon +import com.bobbyesp.spowlo.ui.pages.settings.about.LocalAsset import com.bobbyesp.spowlo.utils.ChromeCustomTabsUtil @@ -36,9 +52,11 @@ fun TrackComponent( artists: String, spotifyUrl: String, hasLyrics: Boolean = false, - isExplicit : Boolean = false, - onClick: () -> Unit = { ChromeCustomTabsUtil.openUrl(spotifyUrl)} + isExplicit: Boolean = false, + onClick: () -> Unit = { ChromeCustomTabsUtil.openUrl(spotifyUrl) } ) { + val clipboardManager = LocalClipboardManager.current + val showDropdown = remember { mutableStateOf(false) } Column( modifier .fillMaxWidth() @@ -81,7 +99,7 @@ fun TrackComponent( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center - ){ + ) { MarqueeText( text = artists, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), @@ -99,7 +117,73 @@ fun TrackComponent( } } } - Icon(imageVector = Icons.Filled.Download, contentDescription = "Download icon", modifier = Modifier.weight(0.1f).padding(6.dp)) + Column { + FilledTonalIconButton(onClick = { + showDropdown.value = !showDropdown.value + }, + modifier = Modifier.size(32.dp), + ) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "More options button", + modifier = Modifier + .weight(0.1f) + .padding(6.dp) + ) + } + + DropdownMenu( + expanded = showDropdown.value, + onDismissRequest = { showDropdown.value = false }, + properties = PopupProperties( + dismissOnClickOutside = true, + dismissOnBackPress = true, + focusable = true, + ), + ) { + DropdownMenuItem( + onClick = onClick, + text = { + Text(text = stringResource(id = R.string.download)) + }, + leadingIcon = { + Icon( + imageVector = Icons.Filled.Download, + contentDescription = "Download icon", + ) + } + ) + Divider() + DropdownMenuItem( + text = { + Text(text = stringResource(id = R.string.open_in_spotify)) + }, onClick = { + ChromeCustomTabsUtil.openUrl(spotifyUrl) + }, + leadingIcon = { + Icon( + imageVector = LocalAsset(id = R.drawable.spotify_logo), + contentDescription = "Spotify logo", + ) + } + ) + + DropdownMenuItem( + onClick = { + clipboardManager.setText(AnnotatedString(spotifyUrl)) + }, + text = { + Text(text = stringResource(id = R.string.copy_link)) + }, + leadingIcon = { + Icon( + imageVector = Icons.Filled.ContentCopy, + contentDescription = "Copy link icon", + ) + } + ) + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt index d1880f74..4fcd96fa 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt @@ -5,7 +5,10 @@ import android.os.Build import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding @@ -13,14 +16,15 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Cancel import androidx.compose.material.icons.outlined.Dataset import androidx.compose.material.icons.outlined.DownloadDone +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -29,9 +33,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController @@ -72,8 +80,7 @@ fun DownloaderBottomSheet( } val checkPermissionOrDownload = { - if (Build.VERSION.SDK_INT > 29 || storagePermission.status == PermissionStatus.Granted) - downloaderViewModel.startDownloadSong() + if (Build.VERSION.SDK_INT > 29 || storagePermission.status == PermissionStatus.Granted) downloaderViewModel.startDownloadSong() else { storagePermission.launchPermissionRequest() } @@ -95,10 +102,55 @@ fun DownloaderBottomSheet( .background(MaterialTheme.colorScheme.background) .navigationBarsPadding() .clip(roundedTopShape) + .padding(8.dp) ) { - //I want to create a pager here with tabs at the top for each page - TabRow(selectedTabIndex = pagerState.currentPage) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.DownloadDone, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp, start = 8.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.settings_before_download), + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier + .padding(vertical = 12.dp), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold + ) + } + Text( + text = stringResource(R.string.settings_before_download_text), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.Start).padding(start = 8.dp) + ) + IndicatorBehindScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + indicator = { tabPositions -> + Box( + Modifier + .padding(vertical = 12.dp) + .tabIndicatorOffset(tabPositions[pagerState.currentPage]) + .fillMaxHeight() + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondaryContainer) + ) + }, + edgePadding = 16.dp, + tabAlignment = Alignment.CenterStart, + ) { pages.forEachIndexed { index, page -> Tab( text = { Text(text = page) }, @@ -107,20 +159,22 @@ fun DownloaderBottomSheet( scope.launch { pagerState.animateScrollToPage(index) } - } + }, ) } } HorizontalPager(pageCount = pages.size, state = pagerState) { when (pages[it]) { BottomSheetPages.MAIN -> { - Text(text = "Main page") + Text(text = "Main page", color = MaterialTheme.colorScheme.onSurface) } + BottomSheetPages.SECONDARY -> { - Text(text = "Secondary page") + Text(text = "Secondary page", color = MaterialTheme.colorScheme.onSurface) } + BottomSheetPages.TERTIARY -> { - Text(text = "Tertiary page") + Text(text = "Tertiary page", color = MaterialTheme.colorScheme.onSurface) } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/PagerUtils.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/PagerUtils.kt new file mode 100644 index 00000000..b2953a1e --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/PagerUtils.kt @@ -0,0 +1,242 @@ +package com.bobbyesp.spowlo.ui.dialogs.bottomsheets + +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Surface +import androidx.compose.material3.TabRowDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun IndicatorBehindScrollableTabRow( + selectedTabIndex: Int, + modifier: Modifier = Modifier, + containerColor: Color = TabRowDefaults.containerColor, + contentColor: Color = TabRowDefaults.contentColor, + edgePadding: Dp = ScrollableTabRowPadding, + tabAlignment: Alignment = Alignment.CenterStart, + indicator: @Composable (tabPositions: List) -> Unit, + tabs: @Composable () -> Unit +) { + Surface( + modifier = modifier, + color = containerColor, + contentColor = contentColor + ) { + val scrollState = rememberScrollState() + val coroutineScope = rememberCoroutineScope() + val scrollableTabData = remember(scrollState, coroutineScope) { + ScrollableTabData( + scrollState = scrollState, + coroutineScope = coroutineScope + ) + } + SubcomposeLayout( + Modifier + .fillMaxWidth() + .wrapContentSize(align = tabAlignment) + .horizontalScroll(scrollState) + .selectableGroup() + .clipToBounds() + ) { constraints -> + val padding = edgePadding.roundToPx() + + val tabMeasurables = subcompose(TabSlots.Tabs, tabs) + + val layoutHeight = tabMeasurables.fold(initial = 0) { curr, measurable -> + maxOf(curr, measurable.maxIntrinsicHeight(Constraints.Infinity)) + } + + val tabConstraints = constraints.copy(minHeight = layoutHeight) + val tabPlaceables = tabMeasurables.map { it.measure(tabConstraints) } + + val layoutWidth = tabPlaceables.fold(initial = padding * 2) { curr, measurable -> + curr + measurable.width + (8.dp.roundToPx()) + } + + // Position the children. + layout(layoutWidth, layoutHeight) { + // Place the tabs + val tabPositions = mutableListOf() + var left = padding + + tabPlaceables.forEach { + tabPositions.add(TabPosition(left = left.toDp(), width = it.width.toDp())) + left += it.width + (8.dp.roundToPx()) + } + + // The indicator container is measured to fill the entire space occupied by the tab + // row, and then placed on top of the divider. + subcompose(TabSlots.Indicator) { + indicator(tabPositions) + }.forEach { + it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0) + } + + tabPlaceables.forEachIndexed { idx, it -> + it.placeRelative(tabPositions[idx].left.roundToPx(), 0) + } + + scrollableTabData.onLaidOut( + density = this@SubcomposeLayout, + edgeOffset = padding, + tabPositions = tabPositions, + selectedTab = selectedTabIndex + ) + } + } + } +} + +private enum class TabSlots { + Tabs, + Divider, + Indicator +} + +private class ScrollableTabData( + private val scrollState: ScrollState, + private val coroutineScope: CoroutineScope +) { + private var selectedTab: Int? = null + + fun onLaidOut( + density: Density, + edgeOffset: Int, + tabPositions: List, + selectedTab: Int + ) { + // Animate if the new tab is different from the old tab, or this is called for the first + // time (i.e selectedTab is `null`). + if (this.selectedTab != selectedTab) { + this.selectedTab = selectedTab + tabPositions.getOrNull(selectedTab)?.let { + // Scrolls to the tab with [tabPosition], trying to place it in the center of the + // screen or as close to the center as possible. + val calculatedOffset = it.calculateTabOffset(density, edgeOffset, tabPositions) + if (scrollState.value != calculatedOffset) { + coroutineScope.launch { + scrollState.animateScrollTo( + calculatedOffset, + animationSpec = ScrollableTabRowScrollSpec + ) + } + } + } + } + } + + /** + * @return the offset required to horizontally center the tab inside this TabRow. + * If the tab is at the start / end, and there is not enough space to fully centre the tab, this + * will just clamp to the min / max position given the max width. + */ + private fun TabPosition.calculateTabOffset( + density: Density, + edgeOffset: Int, + tabPositions: List + ): Int = with(density) { + val totalTabRowWidth = tabPositions.last().right.roundToPx() + edgeOffset + val visibleWidth = totalTabRowWidth - scrollState.maxValue + val tabOffset = left.roundToPx() + val scrollerCenter = visibleWidth / 2 + val tabWidth = width.roundToPx() + val centeredTabOffset = tabOffset - (scrollerCenter - tabWidth / 2) + // How much space we have to scroll. If the visible width is <= to the total width, then + // we have no space to scroll as everything is always visible. + val availableSpace = (totalTabRowWidth - visibleWidth).coerceAtLeast(0) + return centeredTabOffset.coerceIn(0, availableSpace) + } +} + +@Immutable +class TabPosition internal constructor(val left: Dp, val width: Dp) { + val right: Dp get() = left + width + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is androidx.compose.material3.TabPosition) return false + + if (left != other.left) return false + if (width != other.width) return false + + return true + } + + override fun hashCode(): Int { + var result = left.hashCode() + result = 31 * result + width.hashCode() + return result + } + + override fun toString(): String { + return "TabPosition(left=$left, right=$right, width=$width)" + } +} + + +private val ScrollableTabRowMinimumTabWidth = 90.dp + +/** + * The default padding from the starting edge before a tab in a [ScrollableTabRow]. + */ +private val ScrollableTabRowPadding = 52.dp + +/** + * [AnimationSpec] used when scrolling to a tab that is not fully visible. + */ +private val ScrollableTabRowScrollSpec: AnimationSpec = tween( + durationMillis = 250, + easing = FastOutSlowInEasing +) + +fun Modifier.tabIndicatorOffset( + currentTabPosition: TabPosition +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "tabIndicatorOffset" + value = currentTabPosition + } +) { + val currentTabWidth by animateDpAsState( + targetValue = currentTabPosition.width, + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), label = "" + ) + + val indicatorOffset by animateDpAsState( + targetValue = currentTabPosition.left, + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), label = "" + ) + + fillMaxWidth() + .wrapContentSize(Alignment.BottomStart) + .offset(x = indicatorOffset) + .width(currentTabWidth) +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 5a67268f..02d36594 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize @@ -201,8 +200,7 @@ fun InitialEntry( NavigationBar( modifier = Modifier .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) - .navigationBarsPadding() - .height(72.dp), + .navigationBarsPadding(), ) { MainActivity.showInBottomNavigation.forEach { (route, icon) -> val text = when (route) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index 89cfb616..b378e71b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -45,7 +45,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -402,7 +404,7 @@ fun FABs( }*/ } -@OptIn(ExperimentalComposeUiApi::class) +@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @Composable fun InputUrl( url: String, @@ -434,7 +436,12 @@ fun InputUrl( focusManager.moveFocus(FocusDirection.Down) onDone() }), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + colors = TextFieldDefaults.outlinedTextFieldColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + 8.dp + ), unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant + ), ) AnimatedVisibility(visible = showDownloadProgress) { Row( diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/mod_downloader/ModsDownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/mod_downloader/ModsDownloaderPage.kt index 1b6e13bd..fb34dc72 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/mod_downloader/ModsDownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/mod_downloader/ModsDownloaderPage.kt @@ -26,8 +26,7 @@ import com.bobbyesp.spowlo.ui.components.PreferenceInfo @OptIn(ExperimentalMaterial3Api::class) @Composable fun ModsDownloaderPage( - onBackPressed: () -> Unit, - modsDownloaderViewModel: ModsDownloaderViewModel + onBackPressed: () -> Unit, modsDownloaderViewModel: ModsDownloaderViewModel ) { val apiResponse = modsDownloaderViewModel.apiResponseFlow.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() @@ -44,8 +43,7 @@ fun ModsDownloaderPage( ) }, navigationIcon = { BackButton { onBackPressed() } - }, actions = { - }, scrollBehavior = scrollBehavior + }, actions = {}, scrollBehavior = scrollBehavior ) }) { paddings -> LazyColumn( @@ -59,8 +57,7 @@ fun ModsDownloaderPage( PreferenceInfo(text = stringResource(id = R.string.mods_advertisement)) } item { - PackagesListItem( - type = PackagesListItemType.Regular, + PackagesListItem(type = PackagesListItemType.Regular, expanded = false, onClick = {}, packages = apps.Regular.sortedByDescending { it.version }, @@ -68,8 +65,7 @@ fun ModsDownloaderPage( ) } item { - PackagesListItem( - type = PackagesListItemType.RegularCloned, + PackagesListItem(type = PackagesListItemType.RegularCloned, expanded = false, onClick = {}, packages = apps.Regular_Cloned.sortedByDescending { it.version }, @@ -77,8 +73,7 @@ fun ModsDownloaderPage( ) } item { - PackagesListItem( - type = PackagesListItemType.Amoled, + PackagesListItem(type = PackagesListItemType.Amoled, expanded = false, onClick = {}, packages = apps.AMOLED.sortedByDescending { it.version }, @@ -86,8 +81,7 @@ fun ModsDownloaderPage( ) } item { - PackagesListItem( - type = PackagesListItemType.AmoledCloned, + PackagesListItem(type = PackagesListItemType.AmoledCloned, expanded = false, onClick = {}, packages = apps.AMOLED_Cloned.sortedByDescending { it.version }, @@ -96,8 +90,7 @@ fun ModsDownloaderPage( } item { - PackagesListItem( - type = PackagesListItemType.Lite, + PackagesListItem(type = PackagesListItemType.Lite, expanded = false, onClick = {}, packages = apps.Lite.sortedByDescending { it.version }, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 750dcebe..3bbbf59f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -194,23 +194,23 @@ fun SearcherPageImpl( val allItems = mutableListOf() //TODO: Add the filters. Pagination should be done in the future viewState.viewState.data.let { data -> - data.albums?.items?.let { allItems.addAll(it) } - data.artists?.items?.let { allItems.addAll(it) } + data.albums?.items?.let {/* allItems.addAll(it)*/ } + data.artists?.items?.let { /*allItems.addAll(it) */} data.playlists?.items?.let { allItems.addAll(it) } data.tracks?.items?.let { allItems.addAll(it) } - data.episodes?.items?.let { + data.episodes?.items?.let {/* allItems.addAll( listOf( it ) - ) + )*/ } - data.shows?.items?.let { + data.shows?.items?.let {/* allItems.addAll( listOf( it ) - ) + )*/ } if (data != null) { //You may think that this is not necessary, but it is item { From c9cc68d6b0b816b4270259708938500f2fc9f0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sun, 26 Mar 2023 23:37:13 +0200 Subject: [PATCH 26/42] beaut: Improved the bottom sheet downloader and added threads slider. Added some more info at tracks page --- .../data/remote/SpotifyApiRequests.kt | 38 ++- .../songs/metadata_viewer/ExtraInfoCard.kt | 37 +++ .../bottomsheets/DownloaderBottomSheet.kt | 299 +++++++++++++++++- .../ui/pages/downloader/DownloaderPage.kt | 5 +- .../binders/SpotifyPageBinder.kt | 2 +- .../pages/metadata_viewer/pages/TrackPage.kt | 35 +- .../metadata_viewer/playlists/PlaylistPage.kt | 8 +- .../settings/general/GeneralSettingsPage.kt | 118 ++++++- .../bobbyesp/spowlo/utils/PreferencesUtil.kt | 3 +- app/src/main/res/values/strings.xml | 5 + 10 files changed, 520 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt index a40b9cc9..b5674316 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt @@ -4,6 +4,7 @@ import android.util.Log import com.adamratzman.spotify.SpotifyAppApi import com.adamratzman.spotify.models.Album import com.adamratzman.spotify.models.Artist +import com.adamratzman.spotify.models.AudioFeatures import com.adamratzman.spotify.models.PagingObject import com.adamratzman.spotify.models.Playlist import com.adamratzman.spotify.models.SpotifyPublicUser @@ -55,8 +56,8 @@ object SpotifyApiRequests { } //Performs Spotify database query for queries related to user information. - suspend fun userSearch(userQuery: String): SpotifyPublicUser? { - return api!!.users.getProfile(userQuery) + private suspend fun userSearch(userQuery: String): SpotifyPublicUser? { + return provideSpotifyApi().users.getProfile(userQuery) } @Provides @@ -68,7 +69,7 @@ object SpotifyApiRequests { // Performs Spotify database query for queries related to track information. suspend fun searchAllTypes(searchQuery: String): SpotifySearchResult { kotlin.runCatching { - api!!.search.searchAllTypes(searchQuery, limit = 50, offset = 1, market = Market.ES) + provideSpotifyApi().search.searchAllTypes(searchQuery, limit = 50, offset = 1, market = Market.ES) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") return SpotifySearchResult() @@ -84,9 +85,9 @@ object SpotifyApiRequests { return searchAllTypes(query) } - suspend fun searchTracks(searchQuery: String): List { + private suspend fun searchTracks(searchQuery: String): List { kotlin.runCatching { - api!!.search.searchTrack(searchQuery, limit = 50, offset = 1, market = Market.ES) + provideSpotifyApi().search.searchTrack(searchQuery, limit = 50, offset = 1, market = Market.ES) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") return listOf() @@ -104,7 +105,7 @@ object SpotifyApiRequests { suspend fun searchTracksForPaging(searchQuery: String, nextPageNumber: Int): PagingObject? { kotlin.runCatching { - api!!.search.searchTrack(searchQuery, limit = 50, offset = nextPageNumber, market = Market.ES) + provideSpotifyApi().search.searchTrack(searchQuery, limit = 50, offset = nextPageNumber, market = Market.ES) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") }.onSuccess { @@ -122,7 +123,7 @@ object SpotifyApiRequests { //search by id suspend fun getPlaylistById(id: String): Playlist? { kotlin.runCatching { - api!!.playlists.getPlaylist(id, market = Market.ES) + provideSpotifyApi().playlists.getPlaylist(id, market = Market.ES) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") return null @@ -140,7 +141,7 @@ object SpotifyApiRequests { suspend fun getTrackById(id: String): Track? { kotlin.runCatching { - api!!.tracks.getTrack(id, market = Market.ES) + provideSpotifyApi().tracks.getTrack(id, market = Market.ES) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") return null @@ -156,7 +157,7 @@ object SpotifyApiRequests { return getTrackById(id) } - suspend fun getArtistById(id: String): Artist? { + private suspend fun getArtistById(id: String): Artist? { kotlin.runCatching { api!!.artists.getArtist(id) }.onFailure { @@ -176,7 +177,7 @@ object SpotifyApiRequests { suspend fun getAlbumById(id: String): Album? { kotlin.runCatching { - api!!.albums.getAlbum(id, market = Market.ES) + provideSpotifyApi().albums.getAlbum(id, market = Market.ES) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") return null @@ -191,4 +192,21 @@ object SpotifyApiRequests { suspend fun providesGetAlbumById(id: String): Album? { return getAlbumById(id) } + + private suspend fun getAudioFeatures(id: String): AudioFeatures? { + kotlin.runCatching { + provideSpotifyApi().tracks.getAudioFeatures(id) + }.onFailure { + Log.d("SpotifyApiRequests", "Error: ${it.message}") + }.onSuccess { + return it + } + return null + } + + @Provides + @Singleton + suspend fun providesGetAudioFeatures(id: String): AudioFeatures? { + return getAudioFeatures(id) + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt index 38bfccb0..225aeb4c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt @@ -2,6 +2,8 @@ package com.bobbyesp.spowlo.ui.components.songs.metadata_viewer import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.CardDefaults @@ -52,6 +54,41 @@ fun ExtraInfoCard( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WideExtraInfoCard( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + headlineText: String = "POPULARITY", + bodyText: String = "69" +) { + OutlinedCard( + onClick = onClick, + shape = MaterialTheme.shapes.medium, + modifier = modifier.fillMaxWidth().height(100.dp), + colors = CardDefaults.outlinedCardColors( + containerColor = Color.Transparent, + ) + ) { + Text( + text = headlineText, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier + .align(alignment = Alignment.CenterHorizontally) + .padding(top = 8.dp) + ) + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = bodyText, + style = MaterialTheme.typography.headlineLarge, + modifier = Modifier, + fontWeight = FontWeight.ExtraBold + ) + } + } +} + @Preview @Composable fun ExtraInfoCardPreview() { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt index 4fcd96fa..bd7a3cef 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt @@ -2,8 +2,12 @@ package com.bobbyesp.spowlo.ui.dialogs.bottomsheets import android.Manifest import android.os.Build +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -16,12 +20,17 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AudioFile import androidx.compose.material.icons.outlined.Cancel import androidx.compose.material.icons.outlined.Dataset import androidx.compose.material.icons.outlined.DownloadDone +import androidx.compose.material.icons.outlined.HighQuality +import androidx.compose.material.icons.outlined.Key +import androidx.compose.material.icons.outlined.Person import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab @@ -43,11 +52,29 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.components.AudioFilterChip +import com.bobbyesp.spowlo.ui.components.ButtonChip +import com.bobbyesp.spowlo.ui.components.DrawerSheetSubtitle import com.bobbyesp.spowlo.ui.components.FilledButtonWithIcon import com.bobbyesp.spowlo.ui.components.OutlinedButtonWithIcon import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel +import com.bobbyesp.spowlo.ui.pages.settings.format.AudioFormatDialog +import com.bobbyesp.spowlo.ui.pages.settings.format.AudioQualityDialog +import com.bobbyesp.spowlo.ui.pages.settings.spotify.SpotifyClientIDDialog +import com.bobbyesp.spowlo.ui.pages.settings.spotify.SpotifyClientSecretDialog +import com.bobbyesp.spowlo.utils.COOKIES +import com.bobbyesp.spowlo.utils.DONT_FILTER_RESULTS +import com.bobbyesp.spowlo.utils.GEO_BYPASS +import com.bobbyesp.spowlo.utils.ORIGINAL_AUDIO +import com.bobbyesp.spowlo.utils.PreferencesUtil +import com.bobbyesp.spowlo.utils.SKIP_INFO_FETCH +import com.bobbyesp.spowlo.utils.SYNCED_LYRICS import com.bobbyesp.spowlo.utils.ToastUtil +import com.bobbyesp.spowlo.utils.USE_CACHING +import com.bobbyesp.spowlo.utils.USE_SPOTIFY_CREDENTIALS +import com.bobbyesp.spowlo.utils.USE_YT_METADATA import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState @@ -64,7 +91,6 @@ fun DownloaderBottomSheet( val pagerState = rememberPagerState(initialPage = 0) val pages = listOf(BottomSheetPages.MAIN, BottomSheetPages.SECONDARY, BottomSheetPages.TERTIARY) - var selectedTabIndex by remember { mutableStateOf(0) } val viewState by downloaderViewModel.viewStateFlow.collectAsStateWithLifecycle() val roundedTopShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 0.dp, bottomEnd = 0.dp) @@ -86,6 +112,77 @@ fun DownloaderBottomSheet( } } + val settings = PreferencesUtil + + var preserveOriginalAudio by remember { + mutableStateOf( + settings.getValue( + ORIGINAL_AUDIO + ) + ) + } + + var useSpotifyCredentials by remember { + mutableStateOf( + settings.getValue( + USE_SPOTIFY_CREDENTIALS + ) + ) + } + + var useYtMetadata by remember { + mutableStateOf( + settings.getValue( + USE_YT_METADATA + ) + ) + } + + var useCookies by remember { + mutableStateOf( + settings.getValue( + COOKIES + ) + ) + } + + var useCaching by remember { + mutableStateOf( + settings.getValue( + USE_CACHING + ) + ) + } + + var dontFilter by remember { + mutableStateOf( + settings.getValue( + DONT_FILTER_RESULTS + ) + ) + } + + var useSyncedLyrics by remember { + mutableStateOf( + settings.getValue(SYNCED_LYRICS) + ) + } + + var useGeoBypass by remember { + mutableStateOf( + settings.getValue( + GEO_BYPASS + ) + ) + } + + var skipInfoFetch by remember { mutableStateOf(settings.getValue(SKIP_INFO_FETCH)) } + + var showAudioFormatDialog by remember { mutableStateOf(false) } + var showAudioQualityDialog by remember { mutableStateOf(false) } + var showClientIdDialog by remember { mutableStateOf(false) } + var showClientSecretDialog by remember { mutableStateOf(false) } + val downloadButtonCallback = { navController.popBackStack() checkPermissionOrDownload() @@ -103,6 +200,12 @@ fun DownloaderBottomSheet( .navigationBarsPadding() .clip(roundedTopShape) .padding(8.dp) + .animateContentSize( + animationSpec = tween( + durationMillis = 300, + easing = FastOutSlowInEasing + ), + ) ) { Row( @@ -138,6 +241,7 @@ fun DownloaderBottomSheet( ) IndicatorBehindScrollableTabRow( selectedTabIndex = pagerState.currentPage, + modifier = Modifier.animateContentSize(), indicator = { tabPositions -> Box( Modifier @@ -163,18 +267,171 @@ fun DownloaderBottomSheet( ) } } - HorizontalPager(pageCount = pages.size, state = pagerState) { + HorizontalPager(pageCount = pages.size, state = pagerState, modifier = Modifier.animateContentSize()) { when (pages[it]) { BottomSheetPages.MAIN -> { - Text(text = "Main page", color = MaterialTheme.colorScheme.onSurface) + Column( + modifier = Modifier.fillMaxWidth().padding(6.dp) + ) { + DrawerSheetSubtitle(text = stringResource(id = R.string.general_settings)) + Row( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background( + color = MaterialTheme.colorScheme.surfaceVariant + ), + ) { + AudioFilterChip( + label = stringResource(id = R.string.preserve_original_audio), + animated = true, + selected = preserveOriginalAudio, + onClick = { + preserveOriginalAudio = !preserveOriginalAudio + scope.launch { + settings.updateValue(ORIGINAL_AUDIO, preserveOriginalAudio) + } + } + ) + ButtonChip( + label = stringResource(id = R.string.audio_format), + icon = Icons.Outlined.AudioFile, + onClick = { showAudioFormatDialog = true }, + ) + ButtonChip( + label = stringResource(id = R.string.audio_quality), + icon = Icons.Outlined.HighQuality, + enabled = !preserveOriginalAudio, + onClick = { showAudioQualityDialog = true }, + ) + } + DrawerSheetSubtitle(text = stringResource(id = R.string.spotify)) + Row( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background( + color = MaterialTheme.colorScheme.surfaceVariant + ), + ) { + AudioFilterChip( + label = stringResource(id = R.string.use_spotify_credentials), + animated = true, + selected = useSpotifyCredentials, + onClick = { + useSpotifyCredentials = !useSpotifyCredentials + scope.launch { + settings.updateValue(USE_SPOTIFY_CREDENTIALS, useSpotifyCredentials) + } + } + ) + ButtonChip( + label = stringResource(id = R.string.client_id), + icon = Icons.Outlined.Person, + enabled = useSpotifyCredentials, + onClick = { showClientIdDialog = true }, + ) + ButtonChip( + label = stringResource(id = R.string.client_secret), + icon = Icons.Outlined.Key, + enabled = useSpotifyCredentials, + onClick = { showClientSecretDialog = true }, + ) + } + + } } BottomSheetPages.SECONDARY -> { - Text(text = "Secondary page", color = MaterialTheme.colorScheme.onSurface) + } BottomSheetPages.TERTIARY -> { - Text(text = "Tertiary page", color = MaterialTheme.colorScheme.onSurface) + Column( + modifier = Modifier.fillMaxWidth().padding(6.dp) + ) { + DrawerSheetSubtitle(text = stringResource(id = R.string.experimental_features)) + Row( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background( + color = MaterialTheme.colorScheme.surfaceVariant + ), + ) { + AudioFilterChip( + label = stringResource(id = R.string.synced_lyrics), + animated = true, + selected = useSyncedLyrics, + onClick = { + useSyncedLyrics = !useSyncedLyrics + scope.launch { + settings.updateValue(SYNCED_LYRICS, useSyncedLyrics) + } + } + ) + AudioFilterChip( + label = stringResource(id = R.string.geo_bypass), + selected = useGeoBypass, + animated = true, + onClick = { + useGeoBypass = !useGeoBypass + scope.launch { + settings.updateValue(GEO_BYPASS, useGeoBypass) + } + } + ) + + AudioFilterChip( + label = stringResource(id = R.string.use_cache), + animated = true, + selected = useCaching, + onClick = { + useCaching = !useCaching + scope.launch { + settings.updateValue(USE_CACHING, useCaching) + } + } + ) + + AudioFilterChip( + label = stringResource(id = R.string.dont_filter_results), + selected = dontFilter, + animated = true, + onClick = { + dontFilter = !dontFilter + scope.launch { + settings.updateValue(DONT_FILTER_RESULTS, dontFilter) + } + } + ) + AudioFilterChip( + label = stringResource(id = R.string.use_cookies), + animated = true, + selected = useCookies, + onClick = { + useCookies = !useCookies + scope.launch { + settings.updateValue(COOKIES, useCookies) + } + } + ) + AudioFilterChip( + label = stringResource(id = R.string.use_yt_metadata), + animated = true, + selected = useYtMetadata, + onClick = { + useYtMetadata = !useYtMetadata + scope.launch { + settings.updateValue(USE_YT_METADATA, useYtMetadata) + } + } + ) + } + } } } } @@ -217,10 +474,36 @@ fun DownloaderBottomSheet( } } } + + if (showAudioFormatDialog) { + AudioFormatDialog( + onDismissRequest = { showAudioFormatDialog = false }, + ) + } + if (showAudioQualityDialog) { + AudioQualityDialog( + onDismissRequest = { showAudioQualityDialog = false }, + ) + } + if (showClientIdDialog) { + SpotifyClientIDDialog { + showClientIdDialog = !showClientIdDialog + } + } + if (showClientSecretDialog) { + SpotifyClientSecretDialog { + showClientSecretDialog = !showClientSecretDialog + } + } } object BottomSheetPages { - const val MAIN = "main" - const val SECONDARY = "secondary" - const val TERTIARY = "tertiary" + val MAIN = getString(R.string.audio) + val SECONDARY = "secondary" + val TERTIARY = getString(R.string.experimental_features) +} + +//GET STRING FROM APP.CONTEXT GIVEN A r.string ID +fun getString(id: Int): String { + return App.context.getString(id) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index b378e71b..bf90fc85 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -86,6 +86,7 @@ import com.bobbyesp.spowlo.ui.components.NavigationBarSpacer import com.bobbyesp.spowlo.ui.components.songs.SongCard import com.bobbyesp.spowlo.ui.dialogs.DownloaderSettingsDialog import com.bobbyesp.spowlo.ui.pages.settings.about.LocalAsset +import com.bobbyesp.spowlo.ui.theme.harmonizeWith import com.bobbyesp.spowlo.utils.CONFIGURE import com.bobbyesp.spowlo.utils.CUSTOM_COMMAND import com.bobbyesp.spowlo.utils.DEBUG @@ -440,7 +441,9 @@ fun InputUrl( colors = TextFieldDefaults.outlinedTextFieldColors( containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( 8.dp - ), unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant + ), + unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant, + errorContainerColor = MaterialTheme.colorScheme.errorContainer.harmonizeWith(other = MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp)), ), ) AnimatedVisibility(visible = showDownloadProgress) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt index ab6c318c..23206aaa 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt @@ -46,7 +46,7 @@ fun SpotifyPageBinder( SpotifyDataType.TRACK -> { val track = data as? Track track?.let { - TrackPage(track, modifier, trackDownloadCallback) + TrackPage(track,modifier,trackDownloadCallback) } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index 1c56551b..a67dd9c2 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -1,5 +1,6 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -15,6 +16,10 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -24,8 +29,10 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.adamratzman.spotify.models.AudioFeatures import com.adamratzman.spotify.models.Track import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.features.spotify_api.data.remote.SpotifyApiRequests import com.bobbyesp.spowlo.ui.common.AsyncImageImpl import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.MarqueeText @@ -41,6 +48,14 @@ fun TrackPage( trackDownloadCallback: (String) -> Unit, ) { val localConfig = LocalConfiguration.current + val audioFeatures by remember { + mutableStateOf(mutableStateOf(null)) + } + + LaunchedEffect(Unit){ + val feats = SpotifyApiRequests.providesGetAudioFeatures(data.id) + audioFeatures.value = feats + } Column( modifier = modifier.fillMaxSize() ) { @@ -126,7 +141,25 @@ fun TrackPage( bodyText = GeneralTextUtils.convertDuration(data.durationMs.toDouble()), modifier = Modifier.weight(1f) ) - + } + AnimatedVisibility(visible = audioFeatures.value != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + ExtraInfoCard( + headlineText = stringResource(id = R.string.loudness), + bodyText = audioFeatures.value!!.loudness.toString(), + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(16.dp)) + ExtraInfoCard( + headlineText = stringResource(id = R.string.tempo), + bodyText = audioFeatures.value!!.tempo.toString() + " BPM", + modifier = Modifier.weight(1f) + ) + } } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt index 2d83d037..993d5382 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt @@ -77,9 +77,13 @@ fun PlaylistPage( .fillMaxSize()) { item{ Box(Modifier.animateItemPlacement()) { - SpotifyPageBinder(data = state.data, type = typeOfSpotifyDataType(type), modifier = Modifier, trackDownloadCallback = { url -> + SpotifyPageBinder( + data = state.data, + type = typeOfSpotifyDataType(type), + modifier = Modifier, + trackDownloadCallback = { url -> playlistPageViewModel.downloadTrack(url) - } ) + }, ) } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt index acdb35ef..e93c5210 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt @@ -1,6 +1,10 @@ package com.bobbyesp.spowlo.ui.pages.settings.general +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons @@ -11,7 +15,9 @@ import androidx.compose.material.icons.outlined.MyLocation import androidx.compose.material.icons.outlined.Print import androidx.compose.material.icons.outlined.PrintDisabled import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -22,18 +28,23 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import com.bobbyesp.library.SpotDL import com.bobbyesp.library.SpotDLRequest import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.booleanState +import com.bobbyesp.spowlo.ui.common.intState import com.bobbyesp.spowlo.ui.components.BackButton +import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.LargeTopAppBar import com.bobbyesp.spowlo.ui.components.PreferenceItem import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle @@ -42,6 +53,8 @@ import com.bobbyesp.spowlo.utils.DEBUG import com.bobbyesp.spowlo.utils.DONT_FILTER_RESULTS import com.bobbyesp.spowlo.utils.GEO_BYPASS import com.bobbyesp.spowlo.utils.PreferencesUtil +import com.bobbyesp.spowlo.utils.PreferencesUtil.updateInt +import com.bobbyesp.spowlo.utils.THREADS import com.bobbyesp.spowlo.utils.USE_CACHING import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -82,20 +95,25 @@ fun GeneralSettingsPage( ) } + var threadsNumber = THREADS.intState + val loadingString = App.context.getString(R.string.loading) - var spotDLVersion by remember { mutableStateOf( - loadingString - ) } + var spotDLVersion by remember { + mutableStateOf( + loadingString + ) + } //create a non-blocking coroutine to get the version LaunchedEffect(Unit) { GlobalScope.launch { try { withContext(Dispatchers.IO) { - spotDLVersion = SpotDL.getInstance().execute(SpotDLRequest().addOption("-v"), null, null).output + spotDLVersion = SpotDL.getInstance() + .execute(SpotDLRequest().addOption("-v"), null, null).output } - }catch (e: Exception) { + } catch (e: Exception) { spotDLVersion = e.message ?: e.toString() } @@ -146,7 +164,7 @@ fun GeneralSettingsPage( ) } - item{ + item { PreferenceSubtitle(text = stringResource(id = R.string.library_settings)) } item { @@ -191,6 +209,94 @@ fun GeneralSettingsPage( isChecked = dontFilter ) } + item { + HorizontalDivider() + } + item { + /*PreferenceItem( + title = stringResource(id = R.string.threads_number), + description = stringResource(id = R.string.threads_number_desc), + icon = Icons.Outlined.Settings, + onClick = { + scope.launch { + val result = MaterialDialog(context).show { + title(text = stringResource(id = R.string.threads_number)) + message(text = stringResource(id = R.string.threads_number_desc)) + input( + hint = stringResource(id = R.string.threads_number), + prefill = threadsNumber.toString(), + inputType = InputType.TYPE_CLASS_NUMBER + ) { _, text -> + threadsNumber = text.toString().toInt() + PreferencesUtil.updateValue(THREADS_NUMBER, threadsNumber) + } + positiveButton(text = stringResource(id = R.string.ok)) + negativeButton(text = stringResource(id = R.string.cancel)) + } + } + }, + onClickLabel = stringResource(id = R.string.update), + onLongClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + + }, onLongClickLabel = stringResource(id = R.string.open_settings) + )*/ + //threads number item with a slicer + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.threads), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(start = 16.dp, top = 16.dp) + .weight(1f) + ) + Text( + text = stringResource(id = R.string.threads_number) + ": " + threadsNumber.value.toString(), + style = MaterialTheme.typography.labelLarge.copy( + color = MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.6f + ) + ), + modifier = Modifier.padding(end = 16.dp, top = 16.dp) + ) + } + Text( + text = stringResource(id = R.string.threads_number_desc), + modifier = Modifier.padding( + vertical = 12.dp, + horizontal = 16.dp + ), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + ) + + } + } + Slider( + value = threadsNumber.value.toFloat(), + onValueChange = { + threadsNumber.value = it.toInt() + THREADS.updateInt(it.toInt()) + }, + valueRange = 1f..10f, + steps = 9, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } } }) } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt index 92720cfc..014d9cab 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt @@ -11,7 +11,6 @@ import com.bobbyesp.spowlo.App.Companion.applicationScope import com.bobbyesp.spowlo.App.Companion.context import com.bobbyesp.spowlo.App.Companion.isFDroidBuild import com.bobbyesp.spowlo.R -import com.bobbyesp.spowlo.database.CommandTemplate import com.bobbyesp.spowlo.database.CookieProfile import com.bobbyesp.spowlo.ui.pages.settings.about.LocalAsset import com.bobbyesp.spowlo.ui.theme.DEFAULT_SEED_COLOR @@ -63,6 +62,7 @@ const val USE_CACHING = "use_caching" const val DONT_FILTER_RESULTS = "dont_filter_results" const val GEO_BYPASS = "geo-bypass" const val AUDIO_PROVIDER = "audio_provider" +const val THREADS = "threads" const val SPOTDL_UPDATE = "spotdl_update" const val TEMPLATE_ID = "template_id" @@ -118,6 +118,7 @@ private val IntPreferenceDefaults = mapOf( AUDIO_QUALITY to 17, AUDIO_PROVIDER to 0, UPDATE_CHANNEL to STABLE, + THREADS to 1, ) val palettesMap = mapOf( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a6228bb3..1ce7e899 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -284,4 +284,9 @@ Duration Download started Download finished + Loudness + Tempo + Threads + Change how many downloads at a time can be running. This doesn\'t apply on single track downloads. + Number of threads \ No newline at end of file From 23148e46882bb405d0a83799b33c2ae85aa3d0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= <60316747+BobbyESP@users.noreply.github.com> Date: Mon, 27 Mar 2023 00:07:04 +0200 Subject: [PATCH 27/42] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 4b6cf145..93468dda 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,11 @@ For most devices, it is recommended to install the **ARM64-v8a** version of the - Download the latest stable version from [GitHub releases](https://github.com/BobbyESP/Spowlo/releases/latest) +## Translation + +We are using Hosted Weblate for the translations of the app. if you want to contribute follow [this link](https://hosted.weblate.org/engage/spowlo/) 🖇️ + + ## 📖Credits Thanks to [xnetcat](https://github.com/xnetcat) for it's help with some spotDL related things! From fcf3979674946d23102188d1953ae72c6883e65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 27 Mar 2023 08:03:06 +0200 Subject: [PATCH 28/42] bugfix: fixed the app from crashing when trying to put a song in the database --- .../com/bobbyesp/spowlo/utils/DownloaderUtil.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index 2e96a36f..d1390fdf 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -366,10 +366,14 @@ object DownloaderUtil { private fun insertInfoIntoDownloadHistory( songInfo: Song, filePaths: List ) { + filePaths.forEach { filePath -> + val fullString = StringBuilder() + fullString.append(songInfo.name) + fullString.append(filePath) DatabaseUtil.insertInfo( DownloadedSongInfo( - id = songInfo.name.toInt() + filePath.toInt(), + id = createIntFromString(fullString.toString()), songName = songInfo.name, songAuthor = songInfo.artist, songUrl = songInfo.url, @@ -382,6 +386,14 @@ object DownloaderUtil { } } + fun createIntFromString(string: String): Int { + var int = 0 + for (i in string.indices) { + int += string[i].code + } + return int + } + fun executeParallelDownload(url: String) { val taskId = Downloader.makeKey(url = url, randomString = getRandomUUID()) From 585d21eb1a5e4002b0e7929d29455d39b60c5d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 27 Mar 2023 17:03:29 +0200 Subject: [PATCH 29/42] feat: Added tasks list (WIP but functional) --- .../java/com/bobbyesp/spowlo/Downloader.kt | 31 ++- .../java/com/bobbyesp/spowlo/MainActivity.kt | 2 + .../com/bobbyesp/spowlo/ui/common/Route.kt | 6 +- .../spowlo/ui/components/SharedText.kt | 25 +- .../download_tasks/DownloadingTaskItem.kt | 237 ++++++++++++++++++ .../spowlo/ui/components/songs/SongCard.kt | 2 +- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 81 ++++-- .../pages/download_tasks/DownloadTasksPage.kt | 106 ++++++++ .../download_tasks/FullscreenConsoleOutput.kt | 132 ++++++++++ .../downloader/FullscreenConsoleOutput.kt | 2 - .../binders/SpotifyPageBinder.kt | 2 +- .../pages/metadata_viewer/pages/TrackPage.kt | 5 +- .../metadata_viewer/playlists/PlaylistPage.kt | 4 +- .../playlists/PlaylistPageViewModel.kt | 4 +- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 82 +++--- app/src/main/res/values/strings.xml | 12 +- 16 files changed, 646 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/FullscreenConsoleOutput.kt delete mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/FullscreenConsoleOutput.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index 00c63e7f..863d9942 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -1,6 +1,5 @@ package com.bobbyesp.spowlo -import android.content.ClipboardManager import android.util.Log import androidx.annotation.CheckResult import androidx.compose.runtime.mutableStateMapOf @@ -49,9 +48,10 @@ object Downloader { val url: String, val consoleOutput: String, val state: State, - val currentLine: String + val currentLine: String, + val taskName : String, ) { - fun toKey(extraString: String) = makeKey(url, extraString) + fun toKey() = makeKey(url, url.reversed()) private fun randomString() = UUID.randomUUID().toString() sealed class State { @@ -62,7 +62,12 @@ object Downloader { } override fun hashCode(): Int { - return (this.url + randomString()).hashCode() + return (this.url + this .url.reverse()).hashCode() + } + + //get the inverse of a string (from end to start) + fun String.reverse(): String { + return this.reversed() } override fun equals(other: Any?): Boolean { @@ -79,7 +84,7 @@ object Downloader { return true } - fun onCopyLog(clipboardManager: ClipboardManager) { + fun onCopyLog(clipboardManager: androidx.compose.ui.platform.ClipboardManager) { clipboardManager.setText(AnnotatedString(consoleOutput)) } @@ -91,7 +96,7 @@ object Downloader { } - fun onCopyError(clipboardManager: ClipboardManager) { + fun onCopyError(clipboardManager: androidx.compose.ui.platform.ClipboardManager) { clipboardManager.setText(AnnotatedString(currentLine)) ToastUtil.makeToast(R.string.error_copied) } @@ -158,20 +163,22 @@ object Downloader { val mutableTaskList = mutableStateMapOf() - fun onTaskStarted(url: String, extraKey: String) = + fun onTaskStarted(url: String, name: String) = DownloadTask( url = url, consoleOutput = "", state = DownloadTask.State.Running(0f), - currentLine = "" + currentLine = "", + taskName = name ).run { - mutableTaskList.put(this.toKey(extraKey), this) + mutableTaskList.put(this.toKey(), this) } fun updateTaskOutput(url: String, line: String, progress: Float) { - val key = makeKey(url) + val key = makeKey(url, url.reversed()) val oldValue = mutableTaskList[key] ?: return val newValue = oldValue.run { + if (currentLine == line || currentLine.contains("...")) return copy( consoleOutput = consoleOutput + line + "\n", currentLine = line, @@ -431,10 +438,10 @@ object Downloader { } } - fun executeParallelDownloadWithUrl(url: String) = + fun executeParallelDownloadWithUrl(url: String, name: String) = applicationScope.launch(Dispatchers.IO) { DownloaderUtil.executeParallelDownload( - url + url, name ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt b/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt index 05848b03..65c02aa1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.FileDownloadDone import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass @@ -124,6 +125,7 @@ class MainActivity : AppCompatActivity() { val showInBottomNavigation = mapOf( Route.DownloaderNavi to Icons.Rounded.Download, Route.SearcherNavi to Icons.Rounded.Search, + Route.DownloadTasksNavi to Icons.Rounded.FileDownloadDone, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt index bcf77ef5..31a900ab 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt @@ -14,8 +14,6 @@ object Route { const val PLAYLIST = "playlist" const val SETTINGS = "settings" const val FORMAT_SELECTION = "format" - const val TASK_LIST = "task_list" - const val TASK_LOG = "task_log" const val PLAYLIST_METADATA_PAGE = "playlist_metadata_page" const val MODS_DOWNLOADER = "mods_downloader" const val SEARCHER = "searcher" @@ -26,7 +24,8 @@ object Route { const val DOCUMENTATION = "documentation" const val MORE_OPTIONS_HOME = "more_options_home" const val SONG_INFO_HISTORY = "song_info_history" - + const val DOWNLOAD_TASKS = "download_tasks" + const val DownloadTasksNavi = "download_tasks_navi" const val PLAYLIST_PAGE = "playlist_page" const val APPEARANCE = "appearance" @@ -38,6 +37,7 @@ object Route { const val CREDITS = "credits" const val LANGUAGES = "languages" const val DOWNLOAD_QUEUE = "queue" + const val FULLSCREEN_LOG = "fullscreen_log" const val DOWNLOAD_FORMAT = "download_format" const val NETWORK_PREFERENCES = "network_preferences" const val COOKIE_PROFILE = "cookie_profile" diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/SharedText.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/SharedText.kt index d7ac2dc6..b4287dc8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/SharedText.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/SharedText.kt @@ -1,12 +1,27 @@ package com.bobbyesp.spowlo.ui.components -import androidx.compose.animation.core.* +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.TargetBasedAnimation +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.width import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.drawWithContent @@ -236,7 +251,8 @@ fun AutoResizableText( modifier: Modifier = Modifier, text: String, textStyle: TextStyle = MaterialTheme.typography.bodySmall, - color: Color = textStyle.color + color: Color = textStyle.color, + maxLines: Int = 1, ) { var resizedTextStyle by remember { mutableStateOf(textStyle) @@ -250,6 +266,7 @@ fun AutoResizableText( Text( text = text, color = color, + maxLines = maxLines, modifier = modifier.drawWithContent { if (shouldDraw) { drawContent() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt new file mode 100644 index 00000000..aef2148c --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt @@ -0,0 +1,237 @@ +package com.bobbyesp.spowlo.ui.components.download_tasks + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material.icons.outlined.RestartAlt +import androidx.compose.material.icons.outlined.UnfoldMore +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.common.LocalDarkTheme +import com.bobbyesp.spowlo.ui.components.AutoResizableText +import com.bobbyesp.spowlo.ui.components.FlatButtonChip +import com.bobbyesp.spowlo.ui.components.MarqueeText +import com.bobbyesp.spowlo.ui.theme.harmonizeWith +import com.bobbyesp.spowlo.ui.theme.harmonizeWithPrimary +import com.kyant.monet.LocalTonalPalettes +import com.kyant.monet.TonalPalettes.Companion.toTonalPalettes +import com.kyant.monet.dynamicColorScheme + +val greenTonalPalettes = Color.Green.toTonalPalettes() + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DownloadingTaskItem( + modifier: Modifier = Modifier, + status: TaskState = TaskState.ERROR, + progress: Float = .85f, + url: String = "https://www.example.com", + header: String = "Faded - Alan Walker", + progressText: String = "[sample] Extracting URL: https://www.example.com\n" + + "[sample] sample: Downloading webpage\n" + + "[sample] sample: Downloading android player API JSON\n" + + "[info] Available automatic captions for sample:" + "[info] Available automatic captions for sample:", + onCopyLog: () -> Unit = {}, + onCopyError: () -> Unit = {}, + onRestart: () -> Unit = {}, + onShowLog: () -> Unit = {}, +) { + CompositionLocalProvider(LocalTonalPalettes provides greenTonalPalettes) { + val greenScheme = dynamicColorScheme(!LocalDarkTheme.current.isDarkTheme()) + val accentColor = MaterialTheme.colorScheme.run { + when (status) { + TaskState.FINISHED -> greenScheme.primary + TaskState.RUNNING -> primary + TaskState.ERROR -> error.harmonizeWithPrimary() + } + } + val containerColor = MaterialTheme.colorScheme.run { + surfaceColorAtElevation(3.dp).harmonizeWith(other = accentColor) + }.copy(alpha = 0.9f) + val contentColor = MaterialTheme.colorScheme.run { + onSurfaceVariant.harmonizeWith(other = accentColor) + } + + val labelText = stringResource( + id = when (status) { + TaskState.FINISHED -> R.string.status_completed + TaskState.RUNNING -> R.string.downloading + TaskState.ERROR -> R.string.error + } + ) + Surface( + color = containerColor, + shape = CardDefaults.shape, + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.semantics(mergeDescendants = true) { }, + verticalAlignment = Alignment.CenterVertically + ) { + when (status) { + TaskState.FINISHED -> { + Icon( + modifier = Modifier + .padding(8.dp) + .size(24.dp), + imageVector = Icons.Filled.CheckCircle, + tint = accentColor, + contentDescription = stringResource(id = R.string.status_completed) + ) + } + + TaskState.RUNNING -> { + val animatedProgress by animateFloatAsState( + targetValue = progress, + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, + label = "" + ) + if (progress < 0) + CircularProgressIndicator( + modifier = Modifier + .padding(8.dp) + .size(24.dp), + strokeWidth = 5.dp, color = accentColor + ) + else + CircularProgressIndicator( + modifier = Modifier + .padding(8.dp) + .size(24.dp), + strokeWidth = 5.dp, + progress = animatedProgress, + color = accentColor + ) + } + + TaskState.ERROR -> { + Icon( + modifier = Modifier + .padding(8.dp) + .size(24.dp), + imageVector = Icons.Filled.Error, + tint = accentColor, + contentDescription = stringResource(id = R.string.error) + ) + } + } + + Column( + Modifier + .padding(horizontal = 8.dp) + .weight(1f) + ) { + MarqueeText( + text = header, + style = MaterialTheme.typography.titleSmall, + color = contentColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Bold, + + ) + Text( + text = url, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + color = contentColor, + overflow = TextOverflow.Ellipsis + ) + } + IconButton( + modifier = Modifier + .align(Alignment.Top) + .semantics(mergeDescendants = true) { }, + onClick = { onShowLog() }, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + imageVector = Icons.Outlined.UnfoldMore, + contentDescription = stringResource( + id = R.string.open_log + ) + ) + } + } + Box( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .padding(top = 4.dp) + .clip(MaterialTheme.shapes.small) + .background(Color.Black.copy(alpha = 0.8f)), + ) { + AutoResizableText( + text = progressText, + modifier = Modifier.padding(8.dp), + textStyle = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + // color is going to be like this: if the system color is dark, then the text color is white, otherwise it's black + color = Color.White, + maxLines = 2 + ) + } + + Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { + FlatButtonChip( + icon = Icons.Outlined.ContentCopy, + label = stringResource(id = R.string.copy_log) + ) { onCopyLog() } + if (status == TaskState.ERROR) { + FlatButtonChip( + icon = Icons.Outlined.ErrorOutline, + label = stringResource(id = R.string.copy_error_report), + iconColor = MaterialTheme.colorScheme.error, + ) { onCopyError() } + FlatButtonChip( + icon = Icons.Outlined.RestartAlt, + label = stringResource(id = R.string.restart_task), + iconColor = MaterialTheme.colorScheme.secondary, + ) { onRestart() } + } + } + } + } + } +} + +enum class TaskState { + FINISHED, RUNNING, ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt index aec61a25..65eaa63e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt @@ -144,7 +144,7 @@ fun SongCard( Box(Modifier.fillMaxWidth()) { val progressAnimationValue by animateFloatAsState( targetValue = progress, - animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, label = "" ) if (progress < 0f) LinearProgressIndicator( diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 02d36594..b81efb9d 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -58,12 +58,17 @@ import com.bobbyesp.spowlo.features.mod_downloader.data.remote.ModsDownloaderAPI import com.bobbyesp.spowlo.features.spotify_api.data.remote.SpotifyApiRequests import com.bobbyesp.spowlo.ui.common.LocalWindowWidthState import com.bobbyesp.spowlo.ui.common.Route +import com.bobbyesp.spowlo.ui.common.Route.MARKDOWN_VIEWER import com.bobbyesp.spowlo.ui.common.animatedComposable import com.bobbyesp.spowlo.ui.common.animatedComposableVariant +import com.bobbyesp.spowlo.ui.common.arg +import com.bobbyesp.spowlo.ui.common.id import com.bobbyesp.spowlo.ui.common.slideInVerticallyComposable import com.bobbyesp.spowlo.ui.dialogs.UpdaterBottomDrawer import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.DownloaderBottomSheet import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.MoreOptionsHomeBottomSheet +import com.bobbyesp.spowlo.ui.pages.download_tasks.DownloadTasksPage +import com.bobbyesp.spowlo.ui.pages.download_tasks.FullscreenConsoleOutput import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderPage import com.bobbyesp.spowlo.ui.pages.downloader.DownloaderViewModel import com.bobbyesp.spowlo.ui.pages.history.DownloadsHistoryPage @@ -172,7 +177,11 @@ fun InitialEntry( if (isUrlShared) { if (navController.currentDestination?.route != Route.DOWNLOADER) { - navController.popBackStack(route = Route.DOWNLOADER, inclusive = false, saveState = true) + navController.popBackStack( + route = Route.DOWNLOADER, + inclusive = false, + saveState = true + ) } } Box( @@ -206,6 +215,7 @@ fun InitialEntry( val text = when (route) { Route.DownloaderNavi -> App.context.getString(R.string.downloader) Route.SearcherNavi -> App.context.getString(R.string.searcher) + Route.DownloadTasksNavi -> App.context.getString(R.string.tasks) else -> "" } @@ -267,23 +277,31 @@ fun InitialEntry( navigation(startDestination = Route.DOWNLOADER, route = Route.DownloaderNavi) { animatedComposable(Route.DOWNLOADER) { DownloaderPage( - navigateToDownloads = { navController.navigate(Route.DOWNLOADS_HISTORY){ - launchSingleTop = true - } }, - navigateToSettings = { navController.navigate(Route.MORE_OPTIONS_HOME) { - launchSingleTop = true - }}, - navigateToDownloaderSheet = { navController.navigate(Route.DOWNLOADER_SHEET){ - launchSingleTop = true - } }, + navigateToDownloads = { + navController.navigate(Route.DOWNLOADS_HISTORY) { + launchSingleTop = true + } + }, + navigateToSettings = { + navController.navigate(Route.MORE_OPTIONS_HOME) { + launchSingleTop = true + } + }, + navigateToDownloaderSheet = { + navController.navigate(Route.DOWNLOADER_SHEET) { + launchSingleTop = true + } + }, onSongCardClicked = { - navController.navigate(Route.PLAYLIST_METADATA_PAGE){ + navController.navigate(Route.PLAYLIST_METADATA_PAGE) { + launchSingleTop = true + } + }, + navigateToMods = { + navController.navigate(Route.MODS_DOWNLOADER) { launchSingleTop = true } }, - navigateToMods = { navController.navigate(Route.MODS_DOWNLOADER) { - launchSingleTop = true - }}, downloaderViewModel = downloaderViewModel ) } @@ -380,7 +398,7 @@ fun InitialEntry( } animatedComposable( - "markdown_viewer/{markdownFileName}", + MARKDOWN_VIEWER arg "markdownFileName", arguments = listOf( navArgument( "markdownFileName" @@ -437,7 +455,8 @@ fun InitialEntry( navDeepLink { // Want to go to "markdown_viewer/{markdownFileName}" uriPattern = - StringBuilder().append(navRootUrl).append(Route.PLAYLIST_PAGE).append("/{type}") + StringBuilder().append(navRootUrl).append(Route.PLAYLIST_PAGE) + .append("/{type}") .append("/{id}").toString() } @@ -453,16 +472,18 @@ fun InitialEntry( //We build the route with the type of the destination and the id of it val routeWithIdPattern: String = - StringBuilder().append(Route.PLAYLIST_PAGE).append("/{type}").append("/{id}").toString() + StringBuilder().append(Route.PLAYLIST_PAGE).append("/{type}") + .append("/{id}").toString() //We create the composable with the route and the arguments animatedComposableVariant( routeWithIdPattern, - arguments = listOf(typeArg ,idArg) + arguments = listOf(typeArg, idArg) ) { backStackEntry -> val id = backStackEntry.arguments?.getString("id") ?: "SOMETHING WENT WRONG" - val type = backStackEntry.arguments?.getString("type") ?: "SOMETHING WENT WRONG" + val type = backStackEntry.arguments?.getString("type") + ?: "SOMETHING WENT WRONG" PlaylistPage( onBackPressed, @@ -471,6 +492,28 @@ fun InitialEntry( ) } } + + navigation(startDestination = Route.DOWNLOAD_TASKS, route = Route.DownloadTasksNavi) { + + animatedComposable( + Route.FULLSCREEN_LOG arg "taskHashCode", + arguments = listOf(navArgument("taskHashCode") { + type = NavType.IntType + } + )) { + + FullscreenConsoleOutput( + onBackPressed = onBackPressed, + taskHashCode = it.arguments?.getInt("taskHashCode") ?: -1 + ) + } + + animatedComposable(Route.DOWNLOAD_TASKS) { + DownloadTasksPage( + onNavigateToDetail = { navController.navigate(Route.FULLSCREEN_LOG id it) } + ) + } + } } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt new file mode 100644 index 00000000..35f6a212 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt @@ -0,0 +1,106 @@ +package com.bobbyesp.spowlo.ui.pages.download_tasks + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.bobbyesp.spowlo.Downloader +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.components.download_tasks.DownloadingTaskItem +import com.bobbyesp.spowlo.ui.components.download_tasks.TaskState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DownloadTasksPage(onNavigateToDetail: (Int) -> Unit) { + + val scope = rememberCoroutineScope() + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + + Scaffold(modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar(title = { + Text( + text = stringResource(R.string.download_tasks), + style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp) + ) + }, actions = { + }, scrollBehavior = scrollBehavior + ) + }) { paddings -> + val clipboardManager = LocalClipboardManager.current + LazyColumn( + modifier = Modifier.padding(paddings), contentPadding = PaddingValues(24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(Downloader.mutableTaskList.values.toList()) { + it.run { + DownloadingTaskItem( + status = state.toStatus(), + progress = if (state is Downloader.DownloadTask.State.Running) state.progress else 0f, + progressText = currentLine, + url = url, + header = it.taskName, + onCopyError = { + onCopyError(clipboardManager) + }, + onRestart = { + onRestart() + }, onCopyLog = { + onCopyLog(clipboardManager) + }, onShowLog = { + onNavigateToDetail(hashCode()) + } + ) + } + } + } + if (Downloader.mutableTaskList.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + ) { + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.no_running_downloads), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + } + } +} + +private fun Downloader.DownloadTask.State.toStatus(): TaskState = when (this) { + Downloader.DownloadTask.State.Completed -> TaskState.FINISHED + is Downloader.DownloadTask.State.Error -> TaskState.ERROR + is Downloader.DownloadTask.State.Running -> TaskState.RUNNING + else -> { + TaskState.ERROR + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/FullscreenConsoleOutput.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/FullscreenConsoleOutput.kt new file mode 100644 index 00000000..736a29c9 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/FullscreenConsoleOutput.kt @@ -0,0 +1,132 @@ +package com.bobbyesp.spowlo.ui.pages.download_tasks + +import android.util.Log +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material.icons.outlined.RestartAlt +import androidx.compose.material3.Divider +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.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.bobbyesp.spowlo.Downloader +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.components.ButtonChip + +private const val TAG = "TaskLogPage" + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FullscreenConsoleOutput( + onBackPressed: () -> Unit, taskHashCode: Int +) { + Log.d(TAG, "TaskLogPage: $taskHashCode") + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val task = Downloader.mutableTaskList.values.find { it.hashCode() == taskHashCode } ?: return + val clipboardManager = LocalClipboardManager.current + Scaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar(title = { + Text( + text = stringResource(R.string.download_log), + style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp) + ) + }, navigationIcon = { + IconButton(onClick = { onBackPressed() }) { + Icon(Icons.Outlined.Close, stringResource(R.string.close)) + } + }, actions = { + }, scrollBehavior = scrollBehavior + ) + }, bottomBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 4.dp) + .navigationBarsPadding(), + verticalArrangement = Arrangement.Center + ) { + Divider(modifier = Modifier.fillMaxWidth()) + Row( + Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 16.dp) + ) { + task.run { + ButtonChip( + icon = Icons.Outlined.ContentCopy, + label = stringResource(id = R.string.copy_log) + ) { + onCopyLog(clipboardManager) + } + if (state is Downloader.DownloadTask.State.Error) + ButtonChip( + icon = Icons.Outlined.ErrorOutline, + label = stringResource(id = R.string.copy_error_report), + iconColor = MaterialTheme.colorScheme.error, + ) { + onCopyError(clipboardManager) + } + if (state is Downloader.DownloadTask.State.Canceled) + ButtonChip( + icon = Icons.Outlined.RestartAlt, + label = stringResource(id = R.string.restart), + ) { + onRestart() + } + } + } + } + }) { paddings -> + val scrollState = rememberScrollState() + LaunchedEffect(key1 = scrollState.maxValue) { + scrollState.animateScrollTo(scrollState.maxValue) + } + Column( + modifier = Modifier + .padding(paddings) + .padding(horizontal = 24.dp) + .verticalScroll(scrollState) + .horizontalScroll(rememberScrollState()) + ) { + SelectionContainer() { + Text( + modifier = Modifier.widthIn(max = 800.dp), + text = task.consoleOutput, + style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/FullscreenConsoleOutput.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/FullscreenConsoleOutput.kt deleted file mode 100644 index d71ca0bf..00000000 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/FullscreenConsoleOutput.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.bobbyesp.spowlo.ui.pages.downloader - diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt index 23206aaa..8a18c669 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt @@ -18,7 +18,7 @@ fun SpotifyPageBinder( data: Any, type: SpotifyDataType, modifier: Modifier = Modifier, - trackDownloadCallback : (String) -> Unit, + trackDownloadCallback : (String, String) -> Unit, ) { Column(modifier = modifier) { when (type) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index a67dd9c2..246203d8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -45,7 +45,7 @@ import com.bobbyesp.spowlo.utils.GeneralTextUtils fun TrackPage( data: Track, modifier: Modifier, - trackDownloadCallback: (String) -> Unit, + trackDownloadCallback: (String, String) -> Unit, ) { val localConfig = LocalConfiguration.current val audioFeatures by remember { @@ -116,12 +116,13 @@ fun TrackPage( Column( modifier = Modifier.fillMaxWidth() ) { + val taskName = StringBuilder().append(data.name).append(" - ").append(data.artists.joinToString(", ") { it.name }).toString() TrackComponent(contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), songName = data.name, artists = data.artists.joinToString(", ") { it.name }, spotifyUrl = data.externalUrls.spotify!!, isExplicit = data.explicit, - onClick = { trackDownloadCallback(data.externalUrls.spotify!!) }) + onClick = { trackDownloadCallback(data.externalUrls.spotify!!, taskName) }) } Spacer(modifier = Modifier.padding(vertical = 8.dp)) Column(modifier = Modifier.fillMaxWidth()) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt index 993d5382..ef88d3a8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt @@ -81,8 +81,8 @@ fun PlaylistPage( data = state.data, type = typeOfSpotifyDataType(type), modifier = Modifier, - trackDownloadCallback = { url -> - playlistPageViewModel.downloadTrack(url) + trackDownloadCallback = { url, name -> + playlistPageViewModel.downloadTrack(url, name) }, ) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt index 5af02f73..ca899fbd 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt @@ -89,8 +89,8 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { } } - fun downloadTrack(url: String) { - Downloader.executeParallelDownloadWithUrl(url) + fun downloadTrack(url: String, name: String) { + Downloader.executeParallelDownloadWithUrl(url, name) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index d1390fdf..42e6044f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -21,6 +21,7 @@ import com.bobbyesp.spowlo.ui.pages.settings.cookies.Cookie import com.bobbyesp.spowlo.utils.FilesUtil.getCookiesFile import com.bobbyesp.spowlo.utils.FilesUtil.getSdcardTempDir import com.bobbyesp.spowlo.utils.FilesUtil.moveFilesToSdcard +import com.bobbyesp.spowlo.utils.PreferencesUtil.getInt import com.bobbyesp.spowlo.utils.PreferencesUtil.getString import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -68,7 +69,8 @@ object DownloaderUtil { val formatId: String = "", val privateMode: Boolean = PreferencesUtil.getValue(PRIVATE_MODE), val sdcard: Boolean = PreferencesUtil.getValue(SDCARD_DOWNLOAD), - val sdcardUri: String = SDCARD_URI.getString() + val sdcardUri: String = SDCARD_URI.getString(), + val threads: Int = THREADS.getInt() ) object CookieScheme { @@ -287,41 +289,42 @@ object DownloaderUtil { val request = SpotDLRequest() val pathBuilder = StringBuilder() - commonRequest(downloadPreferences, url, request, pathBuilder).apply { - if (useSpotifyPreferences) { - if (spotifyClientID.isEmpty() || spotifyClientSecret.isEmpty()) return Result.failure( - Throwable("Spotify client ID or secret is empty while you have the custom credentials option enabled! \n Please check your settings.") - ) - addOption("--client-id", spotifyClientID) - addOption("--client-secret", spotifyClientSecret) - } - }.runCatching { - SpotDL.getInstance().execute(this, taskId, callback = progressCallback) - }.onFailure { th -> - return if (th.message?.contains("No such file or directory") == true) { - th.printStackTrace() - onFinishDownloading( - this, - songInfo = songInfo, - downloadPath = pathBuilder.toString(), - sdcardUri = sdcardUri - ) - } else { - return Result.failure(th) - } - }.onSuccess { response -> - return when { - response.output.contains("LookupError") -> Result.failure(Throwable("A LookupError occurred. The song wasn't found. Try changing the audio provider in the settings and also disabling the 'Don't filter results' option.")) - response.output.contains("YT-DLP") -> Result.failure(Throwable("An error occurred to yt-dlp while downloading the song. Please, report this issue in GitHub.")) - else -> onFinishDownloading( - this, - songInfo = songInfo, - downloadPath = pathBuilder.toString(), - sdcardUri = sdcardUri - ) + commonRequest(downloadPreferences, url, request, pathBuilder) + .apply { + if (useSpotifyPreferences) { + if (spotifyClientID.isEmpty() || spotifyClientSecret.isEmpty()) return Result.failure( + Throwable("Spotify client ID or secret is empty while you have the custom credentials option enabled! \n Please check your settings.") + ) + addOption("--client-id", spotifyClientID) + addOption("--client-secret", spotifyClientSecret) + } + }.runCatching { + SpotDL.getInstance().execute(this, taskId, callback = progressCallback) + }.onFailure { th -> + return if (th.message?.contains("No such file or directory") == true) { + th.printStackTrace() + onFinishDownloading( + this, + songInfo = songInfo, + downloadPath = pathBuilder.toString(), + sdcardUri = sdcardUri + ) + } else { + return Result.failure(th) + } + }.onSuccess { response -> + return when { + response.output.contains("LookupError") -> Result.failure(Throwable("A LookupError occurred. The song wasn't found. Try changing the audio provider in the settings and also disabling the 'Don't filter results' option.")) + response.output.contains("YT-DLP") -> Result.failure(Throwable("An error occurred to yt-dlp while downloading the song. Please, report this issue in GitHub.")) + else -> onFinishDownloading( + this, + songInfo = songInfo, + downloadPath = pathBuilder.toString(), + sdcardUri = sdcardUri + ) + } } - } return onFinishDownloading( this, songInfo = songInfo, @@ -395,18 +398,21 @@ object DownloaderUtil { } - fun executeParallelDownload(url: String) { - val taskId = Downloader.makeKey(url = url, randomString = getRandomUUID()) + fun executeParallelDownload(url: String, name: String) { + val taskId = Downloader.makeKey(url = url, randomString = url.reversed()) ToastUtil.makeToastSuspend(context.getString(R.string.download_started_msg)) val pathBuilder = StringBuilder() val downloadPreferences = DownloadPreferences() - val request = commonRequest(downloadPreferences, url, SpotDLRequest(), pathBuilder) + val request = commonRequest(downloadPreferences, url, SpotDLRequest(), pathBuilder).apply { + addOption("--threads", downloadPreferences.threads.toString()) + } onProcessStarted() - onTaskStarted(url, taskId) + onTaskStarted(url, name) kotlin.runCatching { val response = SpotDL.getInstance().execute(request, taskId) { progress, _, text -> + Log.d(TAG, "executeParallelDownload: $progress $text") Downloader.updateTaskOutput( url = url, line = text, progress = progress ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ce7e899..e4f41157 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -254,7 +254,7 @@ Audio provider Choose from where you want to download the songs Calling the MODs API wasn\'t successful - This mods are hosted by me. If you cannot download them, please, report it in the Telegram channel if possible! + This mods are hosted by me. If you cannot download them, please, report it in the Telegram chatroom if possible! SpotDL is up to date Geo bypass Use a localization bypass to download songs from countries that YT Music is restricted. @@ -289,4 +289,14 @@ Threads Change how many downloads at a time can be running. This doesn\'t apply on single track downloads. Number of threads + Download log + Copy full log + Copy error report + Restart + There is no running downloads + Downloading + Open log + Restart task + Download tasks + Tasks \ No newline at end of file From ccdfc357da7a662ef40151e7dc53d425dc0c271c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 27 Mar 2023 22:27:27 +0200 Subject: [PATCH 30/42] feat: Added notifications (WIP) and something more, dont remember --- app/src/main/java/com/bobbyesp/spowlo/App.kt | 64 ++--- .../java/com/bobbyesp/spowlo/Downloader.kt | 100 ++++--- .../spowlo/DownloaderKeepUpService.kt | 43 +++ .../spowlo/NotificationActionReceiver.kt | 67 +++++ .../java/com/bobbyesp/spowlo/ui/common/Ext.kt | 4 + .../download_tasks/DownloadingTaskItem.kt | 12 +- .../spowlo/ui/pages/common/ErrorPage.kt | 2 +- .../download_tasks/FullscreenConsoleOutput.kt | 52 +++- .../spowlo/ui/pages/searcher/SearcherPage.kt | 2 +- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 20 +- .../spowlo/utils/NotificationsUtil.kt | 245 ++++++++++++++++++ .../main/res/drawable/outline_cancel_24.xml | 10 + .../res/drawable/outline_content_copy_24.xml | 10 + app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values/strings.xml | 14 +- 15 files changed, 568 insertions(+), 79 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/DownloaderKeepUpService.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt create mode 100644 app/src/main/java/com/bobbyesp/spowlo/utils/NotificationsUtil.kt create mode 100644 app/src/main/res/drawable/outline_cancel_24.xml create mode 100644 app/src/main/res/drawable/outline_content_copy_24.xml diff --git a/app/src/main/java/com/bobbyesp/spowlo/App.kt b/app/src/main/java/com/bobbyesp/spowlo/App.kt index 5ad52d2a..a4aa2754 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/App.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/App.kt @@ -4,12 +4,16 @@ import android.annotation.SuppressLint import android.app.Application import android.content.ClipData import android.content.ClipboardManager +import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.net.ConnectivityManager import android.os.Build import android.os.Environment +import android.os.IBinder import android.os.Looper import androidx.core.content.getSystemService import com.bobbyesp.ffmpeg.FFmpeg @@ -83,36 +87,36 @@ class App : Application() { const val userAgentHeader = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Mobile Safari/537.36 Edg/105.0.1343.53" - /* var isServiceRunning = false - - private val connection = object : ServiceConnection { - override fun onServiceConnected(className: ComponentName, service: IBinder) { - val binder = service as DownloadService.DownloadServiceBinder - isServiceRunning = true - } - - override fun onServiceDisconnected(arg0: ComponentName) { - } - } - - fun startService() { - if (isServiceRunning) return - Intent(context.applicationContext, DownloadService::class.java).also { intent -> - context.applicationContext.bindService(intent, connection, Context.BIND_AUTO_CREATE) - } - } - - fun stopService() { - if (!isServiceRunning) return - try { - isServiceRunning = false - context.applicationContext.run { - unbindService(connection) - } - } catch (e: Exception) { - e.printStackTrace() - } - }*/ + var isServiceRunning = false + + private val connection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + val binder = service as DownloaderKeepUpService.DownloadServiceBinder + isServiceRunning = true + } + + override fun onServiceDisconnected(arg0: ComponentName) { + } + } + + fun startService() { + if (isServiceRunning) return + Intent(context.applicationContext, DownloaderKeepUpService::class.java).also { intent -> + context.applicationContext.bindService(intent, connection, Context.BIND_AUTO_CREATE) + } + } + + fun stopService() { + if (!isServiceRunning) return + try { + isServiceRunning = false + context.applicationContext.run { + unbindService(connection) + } + } catch (e: Exception) { + e.printStackTrace() + } + } fun getPrivateDownloadDirectory(): String = diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index 863d9942..90faebd0 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -1,5 +1,6 @@ package com.bobbyesp.spowlo +import android.app.PendingIntent import android.util.Log import androidx.annotation.CheckResult import androidx.compose.runtime.mutableStateMapOf @@ -8,14 +9,19 @@ import com.bobbyesp.library.SpotDL import com.bobbyesp.library.dto.Song import com.bobbyesp.spowlo.App.Companion.applicationScope import com.bobbyesp.spowlo.App.Companion.context +import com.bobbyesp.spowlo.App.Companion.startService +import com.bobbyesp.spowlo.App.Companion.stopService +import com.bobbyesp.spowlo.ui.common.containsEllipsis import com.bobbyesp.spowlo.utils.DownloaderUtil import com.bobbyesp.spowlo.utils.FilesUtil +import com.bobbyesp.spowlo.utils.NotificationsUtil import com.bobbyesp.spowlo.utils.ToastUtil import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.UUID @@ -33,8 +39,8 @@ object Downloader { object Idle : State() } - fun makeKey(url: String, randomString: String = UUID.randomUUID().toString()): String = - "${randomString}_$url" + fun makeKey(url: String, additionalString: String = UUID.randomUUID().toString()): String = + "${additionalString}_$url" data class ErrorState( val errorReport: String = "", @@ -49,11 +55,9 @@ object Downloader { val consoleOutput: String, val state: State, val currentLine: String, - val taskName : String, + val taskName: String, ) { fun toKey() = makeKey(url, url.reversed()) - - private fun randomString() = UUID.randomUUID().toString() sealed class State { data class Error(val errorReport: String) : State() object Completed : State() @@ -62,12 +66,7 @@ object Downloader { } override fun hashCode(): Int { - return (this.url + this .url.reverse()).hashCode() - } - - //get the inverse of a string (from end to start) - fun String.reverse(): String { - return this.reversed() + return (this.url + this.url.reversed()).hashCode() } override fun equals(other: Any?): Boolean { @@ -101,6 +100,13 @@ object Downloader { ToastUtil.makeToast(R.string.error_copied) } + fun onCancel() { + toKey().run { + SpotDL.getInstance().destroyProcessById(this) + onProcessCanceled(this) + } + } + } //---------------------------- @@ -131,12 +137,10 @@ object Downloader { duration = this.duration, isExplicit = this.explicit, hasLyrics = this.lyrics.isNullOrEmpty(), - // fileSizeApprox = this.fileSizeApprox, progress = 0f, progressText = "", thumbnailUrl = this.cover_url, taskId = this.song_id + preferencesHash + playlistIndex, - // playlistIndex = playlistIndex, ) private var currentJob: Job? = null @@ -158,11 +162,31 @@ object Downloader { private val mutableProcessCount = MutableStateFlow(0) private val processCount = mutableProcessCount.asStateFlow() + private val mutableQuickDownloadCount = MutableStateFlow(0) + //------------------------------------- val mutableTaskList = mutableStateMapOf() + init { + applicationScope.launch { + downloaderState.combine(processCount) { state, cnt -> + if (cnt > 0) true + else when (state) { + is State.Idle -> false + else -> true + } + }.combine(mutableQuickDownloadCount) { isRunning, cnt -> + if (!isRunning) cnt > 0 else true + }.collect { + if (it) startService() + else stopService() + } + + } + } + fun onTaskStarted(url: String, name: String) = DownloadTask( url = url, @@ -178,7 +202,7 @@ object Downloader { val key = makeKey(url, url.reversed()) val oldValue = mutableTaskList[key] ?: return val newValue = oldValue.run { - if (currentLine == line || currentLine.contains("...")) return + if (currentLine == line || line.containsEllipsis()) return copy( consoleOutput = consoleOutput + line + "\n", currentLine = line, @@ -192,12 +216,12 @@ object Downloader { url: String, response: String? = null ) { - val key = makeKey(url) - /*NotificationUtil.finishNotification( + val key = makeKey(url, url.reversed()) + NotificationsUtil.finishNotification( notificationId = key.toNotificationId(), title = key, text = context.getString(R.string.status_completed), - )*/ + ) mutableTaskList.run { val oldValue = get(key) ?: return val newValue = oldValue.copy(state = DownloadTask.State.Completed).run { @@ -210,11 +234,11 @@ object Downloader { fun onTaskError(errorReport: String, url: String, extraString: String) = mutableTaskList.run { - val key = makeKey(url, extraString) - /*NotificationUtil.makeErrorReportNotification( + val key = makeKey(url, url.reversed()) + NotificationsUtil.makeErrorReportNotification( notificationId = key.toNotificationId(), error = errorReport - )*/ + ) val oldValue = mutableTaskList[key] ?: return mutableTaskList[key] = oldValue.copy( state = DownloadTask.State.Error( @@ -229,6 +253,16 @@ object Downloader { fun onProcessEnded() = mutableProcessCount.update { it - 1 } + fun onProcessCanceled(taskId: String) = + mutableTaskList.run { + get(taskId)?.let { + this.put( + taskId, + it.copy(state = DownloadTask.State.Canceled) + ) + } + } + fun isDownloaderAvailable(): Boolean { if (downloaderState.value !is State.Idle) { ToastUtil.makeToastSuspend(context.getString(R.string.task_running)) @@ -246,7 +280,7 @@ object Downloader { val isDownloadingPlaylist = downloaderState.value is State.DownloadingPlaylist mutableTaskState.update { songInfo.toTask(preferencesHash = preferences.hashCode()) } - + val notificationId = songInfo.song_id.toInt() + preferences.hashCode() if (!isDownloadingPlaylist) updateState(State.DownloadingSong) return DownloaderUtil.downloadSong( songInfo = songInfo, @@ -257,19 +291,20 @@ object Downloader { mutableTaskState.update { it.copy(progress = progress, progressText = line) } - /*NotificationUtil.notifyProgress( + + NotificationsUtil.notifyProgress( notificationId = notificationId, progress = progress.toInt(), text = line, - title = videoInfo.title - )*/ + title = songInfo.name + ) }.onFailure { if (it is SpotDL.CanceledException) return@onFailure Log.d("Downloader", "The download has been canceled (app thread)") manageDownloadError( it, false, - //notificationId = notificationId, + notificationId = notificationId, isTaskAborted = !isDownloadingPlaylist ) }.onSuccess { @@ -277,9 +312,9 @@ object Downloader { val text = context.getString(if (it.isEmpty()) R.string.status_completed else R.string.download_finish_notification) FilesUtil.createIntentForOpeningFile(it.firstOrNull()).run { - /* NotificationUtil.finishNotification( + NotificationsUtil.finishNotification( notificationId, - title = videoInfo.title, + title = songInfo.name, text = text, intent = if (this != null) PendingIntent.getActivity( context, @@ -287,7 +322,7 @@ object Downloader { this, PendingIntent.FLAG_IMMUTABLE ) else null - )*/ + ) } } } @@ -414,11 +449,11 @@ object Downloader { errorReport = th.message.toString() ) } - notificationId?.let {/* - NotificationUtil.finishNotification( + notificationId?.let { + NotificationsUtil.finishNotification( notificationId = notificationId, text = context.getString(R.string.download_error_msg), - )*/ + ) } if (isTaskAborted) { updateState(State.Idle) @@ -434,7 +469,7 @@ object Downloader { clearProgressState(isFinished = false) taskState.value.taskId.run { SpotDL.getInstance().destroyProcessById(this) - //NotificationUtil.cancelNotification(this.toNotificationId()) + NotificationsUtil.cancelNotification(this.toNotificationId()) } } @@ -446,4 +481,5 @@ object Downloader { } fun onProcessStarted() = mutableProcessCount.update { it + 1 } + fun String.toNotificationId(): Int = this.hashCode() } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/DownloaderKeepUpService.kt b/app/src/main/java/com/bobbyesp/spowlo/DownloaderKeepUpService.kt new file mode 100644 index 00000000..8d33e98b --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/DownloaderKeepUpService.kt @@ -0,0 +1,43 @@ +package com.bobbyesp.spowlo + +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.util.Log +import com.bobbyesp.spowlo.utils.NotificationsUtil +import com.bobbyesp.spowlo.utils.NotificationsUtil.SERVICE_NOTIFICATION_ID + +private val TAG = DownloaderKeepUpService::class.java.simpleName +class DownloaderKeepUpService: Service() { + override fun onBind(intent: Intent): IBinder { + val pendingIntent: PendingIntent = + Intent(this, MainActivity::class.java).let { notificationIntent -> + PendingIntent.getActivity( + this, 0, notificationIntent, + PendingIntent.FLAG_IMMUTABLE + ) + } + val notification = NotificationsUtil.makeServiceNotification(pendingIntent) + startForeground(SERVICE_NOTIFICATION_ID, notification) + return DownloadServiceBinder() + } + + + override fun onUnbind(intent: Intent?): Boolean { + Log.d(TAG, "onUnbind: ") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + stopForeground(true) + } + stopSelf() + return super.onUnbind(intent) + } + + inner class DownloadServiceBinder : Binder() { + fun getService(): DownloaderKeepUpService = this@DownloaderKeepUpService + } +} diff --git a/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt b/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt new file mode 100644 index 00000000..3340c37d --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt @@ -0,0 +1,67 @@ +package com.bobbyesp.spowlo + +import android.content.BroadcastReceiver +import android.content.ClipData +import android.content.Context +import android.content.Intent +import android.util.Log +import com.bobbyesp.library.SpotDL +import com.bobbyesp.spowlo.App.Companion.context +import com.bobbyesp.spowlo.utils.NotificationsUtil +import com.bobbyesp.spowlo.utils.ToastUtil + +class NotificationActionReceiver : BroadcastReceiver() { + companion object { + private const val TAG = "CancelReceiver" + private const val PACKAGE_NAME_PREFIX = "com.junkfood.seal." + + const val ACTION_CANCEL_TASK = 0 + const val ACTION_ERROR_REPORT = 1 + + const val ACTION_KEY = PACKAGE_NAME_PREFIX + "action" + const val TASK_ID_KEY = PACKAGE_NAME_PREFIX + "taskId" + + const val NOTIFICATION_ID_KEY = PACKAGE_NAME_PREFIX + "notificationId" + const val ERROR_REPORT_KEY = PACKAGE_NAME_PREFIX + "error_report" + } + + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null) return + val notificationId = intent.getIntExtra(NOTIFICATION_ID_KEY, 0) + val action = intent.getIntExtra(ACTION_KEY, ACTION_CANCEL_TASK) + Log.d(TAG, "onReceive: $action") + when (action) { + ACTION_CANCEL_TASK -> { + val taskId = intent.getStringExtra(TASK_ID_KEY) + cancelTask(taskId, notificationId) + } + + ACTION_ERROR_REPORT -> { + val errorReport = intent.getStringExtra(ERROR_REPORT_KEY) + if (!errorReport.isNullOrEmpty()) + copyErrorReport(errorReport, notificationId) + } + } + } + + private fun cancelTask(taskId: String?, notificationId: Int) { + if (taskId.isNullOrEmpty()) return + NotificationsUtil.cancelNotification(notificationId) + val result = SpotDL.getInstance().destroyProcessById(taskId) + NotificationsUtil.cancelNotification(notificationId) + if (result) { + Log.d(TAG, "Task (id:$taskId) was killed.") + Downloader.onProcessCanceled(taskId) + + } + } + + private fun copyErrorReport(error: String, notificationId: Int) { + App.clipboard.setPrimaryClip( + ClipData.newPlainText(null, error) + ) + context.let { ToastUtil.makeToastSuspend(it.getString(R.string.error_copied)) } + NotificationsUtil.cancelNotification(notificationId) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Ext.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Ext.kt index 908b8e8d..27a5071b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Ext.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Ext.kt @@ -19,3 +19,7 @@ inline val String.intState @Composable get() = remember { mutableStateOf(this.getInt()) } + +fun String.containsEllipsis(): Boolean { + return this.contains("…") +} diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt index aef2148c..adb42156 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt @@ -16,7 +16,7 @@ import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.RestartAlt -import androidx.compose.material.icons.outlined.UnfoldMore +import androidx.compose.material.icons.outlined.Terminal import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -95,8 +95,10 @@ fun DownloadingTaskItem( } ) Surface( + modifier = modifier, color = containerColor, shape = CardDefaults.shape, + onClick = { onShowLog() }, ) { Column(modifier = Modifier.padding(16.dp)) { Row( @@ -146,7 +148,7 @@ fun DownloadingTaskItem( .size(24.dp), imageVector = Icons.Filled.Error, tint = accentColor, - contentDescription = stringResource(id = R.string.error) + contentDescription = stringResource(id = R.string.searching_error) ) } } @@ -159,7 +161,7 @@ fun DownloadingTaskItem( MarqueeText( text = header, style = MaterialTheme.typography.titleSmall, - color = contentColor, + color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, fontWeight = FontWeight.Bold, @@ -184,7 +186,7 @@ fun DownloadingTaskItem( ) ) { Icon( - imageVector = Icons.Outlined.UnfoldMore, + imageVector = Icons.Outlined.Terminal, contentDescription = stringResource( id = R.string.open_log ) @@ -205,7 +207,7 @@ fun DownloadingTaskItem( textStyle = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), // color is going to be like this: if the system color is dark, then the text color is white, otherwise it's black color = Color.White, - maxLines = 2 + maxLines = 1 ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt index 25bafbd7..38e4efe6 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt @@ -41,7 +41,7 @@ fun ErrorPage( .padding(bottom = 12.dp) ) Text( - stringResource(id = R.string.error), + stringResource(id = R.string.searching_error), modifier = Modifier.align(Alignment.CenterHorizontally) ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/FullscreenConsoleOutput.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/FullscreenConsoleOutput.kt index 736a29c9..6d67ed35 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/FullscreenConsoleOutput.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/FullscreenConsoleOutput.kt @@ -1,6 +1,7 @@ package com.bobbyesp.spowlo.ui.pages.download_tasks import android.util.Log +import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -29,11 +31,19 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.bobbyesp.spowlo.Downloader @@ -51,10 +61,15 @@ fun FullscreenConsoleOutput( val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val task = Downloader.mutableTaskList.values.find { it.hashCode() == taskHashCode } ?: return val clipboardManager = LocalClipboardManager.current + + val minFontSize = 8 + val maxFontSize = 32 + + var mutableFontSize by remember { mutableStateOf(14) } Scaffold( modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { TopAppBar(title = { Text( @@ -90,6 +105,36 @@ fun FullscreenConsoleOutput( ) { onCopyLog(clipboardManager) } + Row( + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .background( + color = Color.Transparent, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.padding(start = 8.dp), + text = stringResource(id = R.string.font_size), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = mutableFontSize.toString(), + style = MaterialTheme.typography.bodyMedium, + fontFamily = FontFamily.Monospace + ) + ButtonChip( + label = "-", + onClick = { mutableFontSize = (mutableFontSize - 2).coerceIn(minFontSize, maxFontSize) } + + ) + ButtonChip( + label = "+", + onClick = { mutableFontSize = (mutableFontSize + 2).coerceIn(minFontSize, maxFontSize) } + ) + } if (state is Downloader.DownloadTask.State.Error) ButtonChip( icon = Icons.Outlined.ErrorOutline, @@ -120,10 +165,11 @@ fun FullscreenConsoleOutput( .verticalScroll(scrollState) .horizontalScroll(rememberScrollState()) ) { - SelectionContainer() { + SelectionContainer { Text( modifier = Modifier.widthIn(max = 800.dp), text = task.consoleOutput, + fontSize = mutableFontSize.sp, style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace) ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 3bbbf59f..5c79dba7 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -178,7 +178,7 @@ fun SearcherPageImpl( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = stringResource(R.string.error), + text = stringResource(R.string.searching_error), style = MaterialTheme.typography.headlineSmall, modifier = Modifier, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index 42e6044f..4fe73d51 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -399,7 +399,7 @@ object DownloaderUtil { fun executeParallelDownload(url: String, name: String) { - val taskId = Downloader.makeKey(url = url, randomString = url.reversed()) + val taskId = Downloader.makeKey(url = url, additionalString = url.reversed()) ToastUtil.makeToastSuspend(context.getString(R.string.download_started_msg)) val pathBuilder = StringBuilder() @@ -417,7 +417,9 @@ object DownloaderUtil { url = url, line = text, progress = progress ) } - onTaskEnded(url, response.output) + //clear all the lines that contains a "…" on it + val finalResponse = removeDuplicateLines(clearLinesWithEllipsis(response.output)) + onTaskEnded(url, finalResponse) }.onFailure { it.printStackTrace() ToastUtil.makeToastSuspend(context.getString(R.string.download_error_msg)) @@ -430,4 +432,18 @@ object DownloaderUtil { onProcessEnded() ToastUtil.makeToastSuspend(context.getString(R.string.download_finished_msg)) } + + fun clearLinesWithEllipsis(input: String): String { + val lines = input.split("\n") + .filterNot { it.contains("…") } + .joinToString("\n") + return lines + } + + fun removeDuplicateLines(input: String): String { + val lines = input.split("\n") + .distinct() + .joinToString("\n") + return lines + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/NotificationsUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/NotificationsUtil.kt new file mode 100644 index 00000000..d1bb528b --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/NotificationsUtil.kt @@ -0,0 +1,245 @@ +package com.bobbyesp.spowlo.utils + +import android.annotation.SuppressLint +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationChannelGroup +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE +import com.bobbyesp.spowlo.App.Companion.context +import com.bobbyesp.spowlo.NotificationActionReceiver +import com.bobbyesp.spowlo.NotificationActionReceiver.Companion.ACTION_CANCEL_TASK +import com.bobbyesp.spowlo.NotificationActionReceiver.Companion.ACTION_ERROR_REPORT +import com.bobbyesp.spowlo.NotificationActionReceiver.Companion.ACTION_KEY +import com.bobbyesp.spowlo.NotificationActionReceiver.Companion.ERROR_REPORT_KEY +import com.bobbyesp.spowlo.NotificationActionReceiver.Companion.NOTIFICATION_ID_KEY +import com.bobbyesp.spowlo.NotificationActionReceiver.Companion.TASK_ID_KEY +import com.bobbyesp.spowlo.R + +private const val TAG = "NotificationUtil" + +@SuppressLint("StaticFieldLeak") +object NotificationsUtil { + private val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private const val PROGRESS_MAX = 100 + private const val PROGRESS_INITIAL = 0 + private const val CHANNEL_ID = "download_notification" + private const val SERVICE_CHANNEL_ID = "download_service" + private const val NOTIFICATION_GROUP_ID = "seal.download.notification" + private const val DEFAULT_NOTIFICATION_ID = 100 + const val SERVICE_NOTIFICATION_ID = 123 + private lateinit var serviceNotification: Notification + + // private var builder = +// NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_stat_seal) + private val commandNotificationBuilder = + NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_launcher_monochrome) + + @RequiresApi(Build.VERSION_CODES.O) + fun createNotificationChannel() { + val name = context.getString(R.string.channel_name) + val descriptionText = context.getString(R.string.channel_description) + val importance = NotificationManager.IMPORTANCE_LOW + val channelGroup = + NotificationChannelGroup(NOTIFICATION_GROUP_ID, context.getString(R.string.download)) + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + group = NOTIFICATION_GROUP_ID + } + val serviceChannel = NotificationChannel(SERVICE_CHANNEL_ID, name, importance).apply { + description = context.getString(R.string.service_title) + group = NOTIFICATION_GROUP_ID + } + notificationManager.createNotificationChannelGroup(channelGroup) + notificationManager.createNotificationChannel(channel) + notificationManager.createNotificationChannel(serviceChannel) + } + + fun notifyProgress( + title: String, + notificationId: Int = DEFAULT_NOTIFICATION_ID, + progress: Int = PROGRESS_INITIAL, + taskId: String? = null, + text: String? = null + ) { + if (!PreferencesUtil.getValue(NOTIFICATION)) return + val pendingIntent = taskId?.let { + Intent(context.applicationContext, NotificationActionReceiver::class.java) + .putExtra(TASK_ID_KEY, taskId) + .putExtra(NOTIFICATION_ID_KEY, notificationId) + .putExtra(ACTION_KEY, ACTION_CANCEL_TASK).run { + PendingIntent.getBroadcast( + context.applicationContext, + notificationId, + this, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + } + } + + NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_launcher_monochrome) + .setContentTitle(title) + .setProgress(PROGRESS_MAX, progress, progress <= 0) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setStyle(NotificationCompat.BigTextStyle().bigText(text)) + .run { + pendingIntent?.let { + addAction( + R.drawable.outline_cancel_24, + context.getString(R.string.cancel), + it + ) + } + notificationManager.notify(notificationId, build()) + } + } + + fun finishNotification( + notificationId: Int = DEFAULT_NOTIFICATION_ID, + title: String? = null, + text: String? = null, + intent: PendingIntent? = null, + ) { + Log.d(TAG, "finishNotification: ") + notificationManager.cancel(notificationId) + if (!PreferencesUtil.getValue(NOTIFICATION)) return + + val builder = + NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_monochrome) + .setContentText(text) + .setOngoing(false) + .setAutoCancel(true) + title?.let { builder.setContentTitle(title) } + intent?.let { builder.setContentIntent(intent) } + notificationManager.notify(notificationId, builder.build()) + } + + fun finishNotificationForCustomCommands( + notificationId: Int = DEFAULT_NOTIFICATION_ID, + title: String? = null, + text: String? = null, + ) { +// notificationManager.cancel(notificationId) + val builder = + NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_monochrome) + .setContentText(text) + .setProgress(0, 0, false) + .setAutoCancel(true) + .setOngoing(false) + .setStyle(null) + title?.let { builder.setContentTitle(title) } + + notificationManager.notify(notificationId, builder.build()) + } + + fun makeServiceNotification(intent: PendingIntent): Notification { + serviceNotification = NotificationCompat.Builder(context, SERVICE_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_monochrome) + .setContentTitle(context.getString(R.string.service_title)) + .setOngoing(true) + .setContentIntent(intent) + .setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE) + .build() + return serviceNotification + } + + fun updateServiceNotification(index: Int, itemCount: Int) { + serviceNotification = NotificationCompat.Builder(context, serviceNotification) + .setContentTitle(context.getString(R.string.service_title) + " ($index/$itemCount)") + .build() + notificationManager.notify(SERVICE_NOTIFICATION_ID, serviceNotification) + } + + fun cancelNotification(notificationId: Int) { + notificationManager.cancel(notificationId) + } + + fun makeErrorReportNotification( + title: String = context.getString(R.string.download_error_msg), + notificationId: Int, + error: String, + ) { + if (!PreferencesUtil.getValue(NOTIFICATION)) return + + val intent = Intent() + .setClass(context, NotificationActionReceiver::class.java) + .putExtra(NOTIFICATION_ID_KEY, notificationId) + .putExtra(ERROR_REPORT_KEY, error) + .putExtra(ACTION_KEY, ACTION_ERROR_REPORT) + + val pendingIntent = PendingIntent.getBroadcast( + context, + notificationId, + intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_launcher_monochrome) + .setContentTitle(title) + .setContentText(error) + .setOngoing(false) + .addAction( + R.drawable.outline_content_copy_24, + context.getString(R.string.copy_error_report), + pendingIntent + ).run { + notificationManager.cancel(notificationId) + notificationManager.notify(notificationId, build()) + } + } + + fun makeNotificationForCustomCommand( + notificationId: Int, + taskId: String, + progress: Int, + text: String? = null, + templateName: String, + taskUrl: String + ) { + if (!PreferencesUtil.getValue(NOTIFICATION)) return + + val intent = Intent(context.applicationContext, NotificationActionReceiver::class.java) + .putExtra(TASK_ID_KEY, taskId) + .putExtra(NOTIFICATION_ID_KEY, notificationId) + .putExtra(ACTION_KEY, ACTION_CANCEL_TASK) + + val pendingIntent = PendingIntent.getBroadcast( + context.applicationContext, + notificationId, + intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + + NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_launcher_monochrome) + .setContentTitle("[${templateName}_${taskUrl}] " + context.getString(R.string.execute_parallel_download)) + .setContentText(text) + .setOngoing(true) + .setProgress(PROGRESS_MAX, progress, progress == -1) + .addAction( + R.drawable.outline_cancel_24, + context.getString(R.string.cancel), + pendingIntent + ) + .run { + notificationManager.notify(notificationId, build()) + } + } + + fun cancelAllNotifications() { + notificationManager.cancelAll() + } + + fun areNotificationsEnabled(): Boolean { + return if (Build.VERSION.SDK_INT <= 24) true else notificationManager.areNotificationsEnabled() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_cancel_24.xml b/app/src/main/res/drawable/outline_cancel_24.xml new file mode 100644 index 00000000..50061cd3 --- /dev/null +++ b/app/src/main/res/drawable/outline_cancel_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_content_copy_24.xml b/app/src/main/res/drawable/outline_content_copy_24.xml new file mode 100644 index 00000000..875963c5 --- /dev/null +++ b/app/src/main/res/drawable/outline_content_copy_24.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 48de8237..9189d510 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -9,7 +9,7 @@ Álbum Mod AMOLED clonado Mod AMOLED - Ocurrió un error en la búsqueda + Ocurrió un error en la búsqueda Una descarga ya está en progreso. Una nueva actualización está disponible! Ocurrió un error al intentar actualizar la aplicación diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4f41157..39ea8050 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -258,7 +258,7 @@ SpotDL is up to date Geo bypass Use a localization bypass to download songs from countries that YT Music is restricted. - An error ocurred while trying to connect to the Spotify API + An error occurred while trying to connect to the Spotify API What would you like to download? No results were found Single @@ -266,16 +266,16 @@ Not specified Default Don\'t convert - An error ocurried while searching + An error occurred while searching Type something on the text box for searching through Spotify! - Realod the page + Reload the page Copy the error Track artwork Album Artist Playlist Track - Shwowing %1$d results + Showing %1$d results Sorry, this page is not yet implemented ;( Go back Both @@ -299,4 +299,10 @@ Restart task Download tasks Tasks + An error occurred + Font size + Download + Show up your downloads on notification centre + Spowlo is downloading + Executing parallel download \ No newline at end of file From c151ccaea42dc72f73fc4b0ed49f4cb04787481a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 27 Mar 2023 23:03:36 +0200 Subject: [PATCH 31/42] bug (not fixed): modified notifications --- app/src/main/AndroidManifest.xml | 1 + app/src/main/java/com/bobbyesp/spowlo/App.kt | 2 ++ app/src/main/java/com/bobbyesp/spowlo/Downloader.kt | 13 ++++++++++++- .../bobbyesp/spowlo/NotificationActionReceiver.kt | 2 +- .../com/bobbyesp/spowlo/utils/NotificationsUtil.kt | 10 +++++----- .../com/bobbyesp/spowlo/utils/PreferencesUtil.kt | 1 + 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3737b598..8df137ed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -101,6 +101,7 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> + \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/App.kt b/app/src/main/java/com/bobbyesp/spowlo/App.kt index a4aa2754..96daaaab 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/App.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/App.kt @@ -23,6 +23,7 @@ import com.bobbyesp.spowlo.utils.DownloaderUtil import com.bobbyesp.spowlo.utils.FilesUtil import com.bobbyesp.spowlo.utils.FilesUtil.createEmptyFile import com.bobbyesp.spowlo.utils.FilesUtil.getCookiesFile +import com.bobbyesp.spowlo.utils.NotificationsUtil import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.PreferencesUtil.getString import com.bobbyesp.spowlo.utils.ToastUtil @@ -73,6 +74,7 @@ class App : Application() { getString(R.string.app_name) ).absolutePath ) + if (Build.VERSION.SDK_INT >= 26) NotificationsUtil.createNotificationChannel() } companion object { diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index 90faebd0..4b66ceb7 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -280,7 +280,7 @@ object Downloader { val isDownloadingPlaylist = downloaderState.value is State.DownloadingPlaylist mutableTaskState.update { songInfo.toTask(preferencesHash = preferences.hashCode()) } - val notificationId = songInfo.song_id.toInt() + preferences.hashCode() + val notificationId = preferences.hashCode() + songInfo.song_id.getNumbers() if (!isDownloadingPlaylist) updateState(State.DownloadingSong) return DownloaderUtil.downloadSong( songInfo = songInfo, @@ -482,4 +482,15 @@ object Downloader { fun onProcessStarted() = mutableProcessCount.update { it + 1 } fun String.toNotificationId(): Int = this.hashCode() + + //get just the numbers from a string and return an int + fun String.getNumbers(): Int { + val sb = StringBuilder() + for (c in this) { + if (c.isDigit()) { + sb.append(c) + } + } + return sb.toString().toInt() + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt b/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt index 3340c37d..9ffa52f8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt @@ -13,7 +13,7 @@ import com.bobbyesp.spowlo.utils.ToastUtil class NotificationActionReceiver : BroadcastReceiver() { companion object { private const val TAG = "CancelReceiver" - private const val PACKAGE_NAME_PREFIX = "com.junkfood.seal." + private const val PACKAGE_NAME_PREFIX = "com.bobbyesp.spowlo." const val ACTION_CANCEL_TASK = 0 const val ACTION_ERROR_REPORT = 1 diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/NotificationsUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/NotificationsUtil.kt index d1bb528b..798383ed 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/NotificationsUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/NotificationsUtil.kt @@ -33,7 +33,7 @@ object NotificationsUtil { private const val PROGRESS_INITIAL = 0 private const val CHANNEL_ID = "download_notification" private const val SERVICE_CHANNEL_ID = "download_service" - private const val NOTIFICATION_GROUP_ID = "seal.download.notification" + private const val NOTIFICATION_GROUP_ID = "spowlo.download.notification" private const val DEFAULT_NOTIFICATION_ID = 100 const val SERVICE_NOTIFICATION_ID = 123 private lateinit var serviceNotification: Notification @@ -124,7 +124,7 @@ object NotificationsUtil { notificationManager.notify(notificationId, builder.build()) } - fun finishNotificationForCustomCommands( + fun finishNotificationForParallelDownloads( notificationId: Int = DEFAULT_NOTIFICATION_ID, title: String? = null, text: String? = null, @@ -198,12 +198,12 @@ object NotificationsUtil { } } - fun makeNotificationForCustomCommand( + fun makeNotificationForParallelDownloads( notificationId: Int, taskId: String, progress: Int, text: String? = null, - templateName: String, + extraString: String, taskUrl: String ) { if (!PreferencesUtil.getValue(NOTIFICATION)) return @@ -221,7 +221,7 @@ object NotificationsUtil { ) NotificationCompat.Builder(context, CHANNEL_ID).setSmallIcon(R.drawable.ic_launcher_monochrome) - .setContentTitle("[${templateName}_${taskUrl}] " + context.getString(R.string.execute_parallel_download)) + .setContentTitle("[${extraString}_${taskUrl}] " + context.getString(R.string.execute_parallel_download)) .setContentText(text) .setOngoing(true) .setProgress(PROGRESS_MAX, progress, progress == -1) diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt index 014d9cab..be7ded4e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt @@ -106,6 +106,7 @@ private val BooleanPreferenceDefaults = SPOTDL_UPDATE to true, GEO_BYPASS to false, SKIP_INFO_FETCH to false, + NOTIFICATION to true, ) private val IntPreferenceDefaults = mapOf( From dc3e305febd909ad73f68da2bf99cdd8b3b54061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Mon, 27 Mar 2023 23:31:48 +0200 Subject: [PATCH 32/42] bugfix: notifications not showing --- app/src/main/java/com/bobbyesp/spowlo/Downloader.kt | 7 +++---- .../spowlo/ui/pages/downloader/DownloaderPage.kt | 10 +++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index 4b66ceb7..7ae01eda 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -119,16 +119,14 @@ object Downloader { val duration: Double = 0.0, val isExplicit: Boolean = false, val hasLyrics: Boolean = false, - // val fileSizeApprox: Long = 0, val progress: Float = 0f, val progressText: String = "", val thumbnailUrl: String = "", val taskId: String = "", val output: String = "", - // val playlistIndex: Int = 0, ) - private fun Song.toTask(playlistIndex: Int = 0, preferencesHash: Int): DownloadTaskItem = + private fun Song.toTask(preferencesHash: Int): DownloadTaskItem = DownloadTaskItem( info = this, spotifyUrl = this.url, @@ -140,7 +138,7 @@ object Downloader { progress = 0f, progressText = "", thumbnailUrl = this.cover_url, - taskId = this.song_id + preferencesHash + playlistIndex, + taskId = this.song_id + preferencesHash, ) private var currentJob: Job? = null @@ -356,6 +354,7 @@ object Downloader { isFetchingInfo = true, isTaskAborted = true ) + return@launch } .onSuccess { info -> for (song in info) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index bf90fc85..37cb050d 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -32,6 +32,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.FormatListBulleted import androidx.compose.material.icons.filled.LibraryMusic +import androidx.compose.material.icons.outlined.Cancel import androidx.compose.material.icons.outlined.ContentPaste import androidx.compose.material.icons.outlined.Error import androidx.compose.material.icons.outlined.FileDownload @@ -172,7 +173,8 @@ fun DownloaderPage( .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { - DownloaderPageImplementation(downloaderState = downloaderState, + DownloaderPageImplementation( + downloaderState = downloaderState, taskState = taskState, viewState = viewState, errorState = errorState, @@ -389,10 +391,8 @@ fun FABs( ) }, modifier = Modifier.padding(vertical = 12.dp) ) - } - - /*AnimatedVisibility(visible = isDownloading) { + AnimatedVisibility(visible = isDownloading) { ExtendedFloatingActionButton( text = { Text(stringResource(R.string.cancel)) }, onClick = cancelCallback, icon = { @@ -402,7 +402,7 @@ fun FABs( ) }, modifier = Modifier.padding(vertical = 12.dp) ) - }*/ + } } @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) From 609ee8d04e351f769bc91d273467ecad93255cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Tue, 28 Mar 2023 22:50:27 +0200 Subject: [PATCH 33/42] feat: started Playlists impl --- .../java/com/bobbyesp/spowlo/Downloader.kt | 3 +- .../ui/pages/downloader/DownloaderPage.kt | 2 - .../metadata_viewer/pages/PlaylistViewPage.kt | 101 +++++++++++++++++- .../pages/metadata_viewer/pages/TrackPage.kt | 3 +- app/src/main/res/values/strings.xml | 1 + 5 files changed, 104 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index 7ae01eda..bc56cff5 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -376,8 +376,7 @@ object Downloader { updateState(State.FetchingInfo) DownloaderUtil.fetchSongInfoFromUrl( url = url - ) - .onFailure { + ).onFailure { manageDownloadError( it, isFetchingInfo = true, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index 37cb050d..c0c6c364 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -89,7 +89,6 @@ import com.bobbyesp.spowlo.ui.dialogs.DownloaderSettingsDialog import com.bobbyesp.spowlo.ui.pages.settings.about.LocalAsset import com.bobbyesp.spowlo.ui.theme.harmonizeWith import com.bobbyesp.spowlo.utils.CONFIGURE -import com.bobbyesp.spowlo.utils.CUSTOM_COMMAND import com.bobbyesp.spowlo.utils.DEBUG import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.PreferencesUtil.getBoolean @@ -192,7 +191,6 @@ fun DownloaderPage( pasteCallback = { matchUrlFromClipboard( string = clipboardManager.getText().toString(), - isMatchingMultiLink = CUSTOM_COMMAND.getBoolean() ).let { downloaderViewModel.updateUrl(it) } }, cancelCallback = { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index ddf2233f..7f95e02b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -1,14 +1,113 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.selection.SelectionContainer +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.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import com.adamratzman.spotify.models.Playlist +import com.bobbyesp.spowlo.App +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.common.AsyncImageImpl +import com.bobbyesp.spowlo.ui.components.HorizontalDivider +import com.bobbyesp.spowlo.ui.components.MarqueeText import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.dataStringToString @Composable fun PlaylistViewPage( data: Playlist, modifier: Modifier ) { - NotImplementedPage() + val localConfig = LocalConfiguration.current + + Column( + modifier = modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .clip(MaterialTheme.shapes.extraSmall) + .fillMaxWidth() + .padding(top = 16.dp, bottom = 6.dp), contentAlignment = Alignment.Center + ) { + //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height + val size = (localConfig.screenHeightDp / 3) + AsyncImageImpl( + modifier = Modifier + .size(size.dp) + .aspectRatio( + 1f, matchHeightConstraintsFirst = true + ) + .clip(MaterialTheme.shapes.small), + model = data.images[0].url, + contentDescription = stringResource(id = R.string.track_artwork), + contentScale = ContentScale.Crop, + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 8.dp) + ) { + SelectionContainer { + MarqueeText( + text = data.name, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineMedium + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = data.owner.displayName ?: data.owner.id, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = dataStringToString( + data = data.type, additional = data.followers.total.toString() + " " + App.context.getString(R.string.followers) + .lowercase() + ), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = data.description ?: "", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) + Column( + modifier = Modifier.fillMaxWidth() + ) { + NotImplementedPage() + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index 246203d8..ac437265 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -117,7 +117,8 @@ fun TrackPage( modifier = Modifier.fillMaxWidth() ) { val taskName = StringBuilder().append(data.name).append(" - ").append(data.artists.joinToString(", ") { it.name }).toString() - TrackComponent(contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + TrackComponent( + contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), songName = data.name, artists = data.artists.joinToString(", ") { it.name }, spotifyUrl = data.externalUrls.spotify!!, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39ea8050..b4858375 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -305,4 +305,5 @@ Show up your downloads on notification centre Spowlo is downloading Executing parallel download + Followers \ No newline at end of file From 1c0518d4b4c2151a3037364792b7e5978254e85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Thu, 30 Mar 2023 22:13:31 +0200 Subject: [PATCH 34/42] feat: Added full Playlist downloading support and more --- .../java/com/bobbyesp/spowlo/Downloader.kt | 11 +- .../data/remote/SpotifyApiRequests.kt | 1 + .../bobbyesp/spowlo/ui/components/Buttons.kt | 4 +- .../spowlo/ui/components/SharedText.kt | 63 +++++++ .../download_tasks/DownloadingTaskItem.kt | 6 + .../components/settings/SettingsComponents.kt | 20 ++- .../songs/metadata_viewer/ExtraInfoCard.kt | 30 +++- .../pages/download_tasks/DownloadTasksPage.kt | 16 +- .../ui/pages/downloader/DownloaderPage.kt | 22 +-- .../binders/SpotifyPageBinder.kt | 2 +- .../metadata_viewer/pages/PlaylistViewPage.kt | 44 ++++- .../pages/metadata_viewer/pages/TrackPage.kt | 3 +- .../settings/cookies/CookiesSettingsPage.kt | 6 +- .../settings/general/GeneralSettingsPage.kt | 160 +++++++----------- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 8 +- app/src/main/res/values/strings.xml | 2 + gradle/libs.versions.toml | 2 +- 17 files changed, 263 insertions(+), 137 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index bc56cff5..e9cbb38f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -85,6 +85,12 @@ object Downloader { fun onCopyLog(clipboardManager: androidx.compose.ui.platform.ClipboardManager) { clipboardManager.setText(AnnotatedString(consoleOutput)) + ToastUtil.makeToastSuspend(context.getString(R.string.log_copied)) + } + + fun onCopyUrl(clipboardManager: androidx.compose.ui.platform.ClipboardManager) { + clipboardManager.setText(AnnotatedString(url)) + ToastUtil.makeToastSuspend(context.getString(R.string.link_copied)) } @@ -200,7 +206,7 @@ object Downloader { val key = makeKey(url, url.reversed()) val oldValue = mutableTaskList[key] ?: return val newValue = oldValue.run { - if (currentLine == line || line.containsEllipsis()) return + if (currentLine == line || line.containsEllipsis() || consoleOutput.contains(line)) return copy( consoleOutput = consoleOutput + line + "\n", currentLine = line, @@ -230,7 +236,7 @@ object Downloader { FilesUtil.scanDownloadDirectoryToMediaLibrary(App.audioDownloadDir) } - fun onTaskError(errorReport: String, url: String, extraString: String) = + fun onTaskError(errorReport: String, url: String) = mutableTaskList.run { val key = makeKey(url, url.reversed()) NotificationsUtil.makeErrorReportNotification( @@ -297,6 +303,7 @@ object Downloader { title = songInfo.name ) }.onFailure { + Log.d("Downloader", "$it") if (it is SpotDL.CanceledException) return@onFailure Log.d("Downloader", "The download has been canceled (app thread)") manageDownloadError( diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt index b5674316..1fb2b4d1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt @@ -128,6 +128,7 @@ object SpotifyApiRequests { Log.d("SpotifyApiRequests", "Error: ${it.message}") return null }.onSuccess { + Log.d("SpotifyApiRequests", "Playlist: $it") return it } return null diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/Buttons.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/Buttons.kt index 7c2bec6c..81a26e56 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/Buttons.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/Buttons.kt @@ -71,11 +71,13 @@ fun TextButtonWithIcon( onClick: () -> Unit, icon: ImageVector, text: String, - contentColor: Color = MaterialTheme.colorScheme.primary + contentColor: Color = MaterialTheme.colorScheme.primary, + enabled : Boolean = true ) { TextButton( modifier = modifier, onClick = onClick, + enabled = enabled, contentPadding = ButtonDefaults.ButtonWithIconContentPadding, colors = ButtonDefaults.textButtonColors(contentColor = contentColor) ) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/SharedText.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/SharedText.kt index b4287dc8..0a7c76b9 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/SharedText.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/SharedText.kt @@ -291,6 +291,69 @@ fun AutoResizableText( ) } +//auto resizable text but with all the text parameters +@Composable +fun AutoResizableText( + modifier: Modifier = Modifier, + text: String, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + maxLines: Int = 1, + style: TextStyle = LocalTextStyle.current.plus(TextStyle()), + color: Color = style.color, +) { + var resizedTextStyle by remember { + mutableStateOf(style) + } + var shouldDraw by remember { + mutableStateOf(false) + } + + val defaultFontSize = MaterialTheme.typography.bodySmall.fontSize + + Text( + text = text, + color = color, + maxLines = maxLines, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + + modifier = modifier.drawWithContent { + if (shouldDraw) { + drawContent() + } + }, + softWrap = false, + style = resizedTextStyle, + onTextLayout = { result -> + if (result.didOverflowWidth) { + if (style.fontSize.isUnspecified) { + resizedTextStyle = resizedTextStyle.copy( + fontSize = defaultFontSize + ) + } + resizedTextStyle = resizedTextStyle.copy( + fontSize = resizedTextStyle.fontSize * 0.95 + ) + } else { + shouldDraw = true + } + } + ) +} + private enum class MarqueeLayers { MainText, SecondaryText, EdgesGradient } private data class TextLayoutInfo(val textWidth: Int, val containerWidth: Int) \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt index adb42156..29f32f76 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt @@ -66,10 +66,12 @@ fun DownloadingTaskItem( "[sample] sample: Downloading webpage\n" + "[sample] sample: Downloading android player API JSON\n" + "[info] Available automatic captions for sample:" + "[info] Available automatic captions for sample:", + artworkUrl: String = "https://www.example.com", onCopyLog: () -> Unit = {}, onCopyError: () -> Unit = {}, onRestart: () -> Unit = {}, onShowLog: () -> Unit = {}, + onCopyLink: () -> Unit = {}, ) { CompositionLocalProvider(LocalTonalPalettes provides greenTonalPalettes) { val greenScheme = dynamicColorScheme(!LocalDarkTheme.current.isDarkTheme()) @@ -216,6 +218,10 @@ fun DownloadingTaskItem( icon = Icons.Outlined.ContentCopy, label = stringResource(id = R.string.copy_log) ) { onCopyLog() } + FlatButtonChip( + icon = Icons.Outlined.ContentCopy, + label = stringResource(id = R.string.copy_link) + ) { onCopyLink() } if (status == TaskState.ERROR) { FlatButtonChip( icon = Icons.Outlined.ErrorOutline, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt index db8cca29..dd7684b7 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt @@ -2,11 +2,14 @@ package com.bobbyesp.spowlo.ui.components.settings import androidx.compose.foundation.clickable import androidx.compose.foundation.selection.toggleable +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -21,7 +24,7 @@ fun SettingsItemNew( description: (@Composable () -> Unit)? = null, trailing: (@Composable () -> Unit)? = null, icon: ImageVector? = null, - addTonalElevation: Boolean = false, + addTonalElevation: Boolean = true, clipCorners: Boolean = false, highlightIcon : Boolean = false ) { @@ -56,7 +59,7 @@ fun SettingsItemNew( description: (@Composable () -> Unit)? = null, trailing: (@Composable () -> Unit)? = null, icon: ImageVector? = null, - addTonalElevation: Boolean = false, + addTonalElevation: Boolean = true, clipCorners: Boolean = false, highlightIcon : Boolean = false ) { @@ -87,7 +90,7 @@ fun SettingsSwitch( description: (@Composable () -> Unit)? = null, icon: ImageVector? = null, thumbContent: (@Composable () -> Unit)? = null, - addTonalElevation: Boolean = false, + addTonalElevation: Boolean = true, clipCorners: Boolean = false, highlightIcon: Boolean = false ) { @@ -117,4 +120,15 @@ fun SettingsSwitch( clipCorners = clipCorners, highlightIcon = highlightIcon ) +} + +@Composable +fun ElevatedSettingsCard( + content : @Composable () -> Unit +) { + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) + ) { + content() + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt index 225aeb4c..11606e1e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/ExtraInfoCard.kt @@ -1,6 +1,8 @@ package com.bobbyesp.spowlo.ui.components.songs.metadata_viewer +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -18,6 +20,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.bobbyesp.spowlo.ui.components.AutoResizableText @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -43,13 +46,22 @@ fun ExtraInfoCard( .align(alignment = Alignment.CenterHorizontally) .padding(top = 8.dp) ) - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text( - text = bodyText, - style = MaterialTheme.typography.headlineLarge, - modifier = Modifier, - fontWeight = FontWeight.ExtraBold - ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.align(alignment = Alignment.Center).padding(10.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + AutoResizableText( + text = bodyText, + style = MaterialTheme.typography.headlineLarge, + modifier = Modifier, + fontWeight = FontWeight.ExtraBold + ) + } } } } @@ -65,7 +77,9 @@ fun WideExtraInfoCard( OutlinedCard( onClick = onClick, shape = MaterialTheme.shapes.medium, - modifier = modifier.fillMaxWidth().height(100.dp), + modifier = modifier + .fillMaxWidth() + .height(100.dp), colors = CardDefaults.outlinedCardColors( containerColor = Color.Transparent, ) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt index 35f6a212..cd1a1a5b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt @@ -21,10 +21,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.bobbyesp.spowlo.Downloader import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.download_tasks.DownloadingTaskItem import com.bobbyesp.spowlo.ui.components.download_tasks.TaskState @@ -71,6 +73,9 @@ fun DownloadTasksPage(onNavigateToDetail: (Int) -> Unit) { onCopyLog(clipboardManager) }, onShowLog = { onNavigateToDetail(hashCode()) + }, + onCopyLink = { + onCopyUrl(clipboardManager) } ) } @@ -81,7 +86,9 @@ fun DownloadTasksPage(onNavigateToDetail: (Int) -> Unit) { modifier = Modifier.fillMaxSize(), ) { Column( - modifier = Modifier.align(Alignment.Center), + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( @@ -89,6 +96,13 @@ fun DownloadTasksPage(onNavigateToDetail: (Int) -> Unit) { style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) + HorizontalDivider( modifier = Modifier.padding(vertical = 24.dp, horizontal = 4.dp)) + Text( + text = stringResource(R.string.no_running_downloads_description), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index c0c6c364..b0d2dbce 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -389,17 +389,17 @@ fun FABs( ) }, modifier = Modifier.padding(vertical = 12.dp) ) - } - AnimatedVisibility(visible = isDownloading) { - ExtendedFloatingActionButton( - text = { Text(stringResource(R.string.cancel)) }, - onClick = cancelCallback, icon = { - Icon( - Icons.Outlined.Cancel, - contentDescription = stringResource(R.string.cancel_download) - ) - }, modifier = Modifier.padding(vertical = 12.dp) - ) + AnimatedVisibility(visible = isDownloading) { + ExtendedFloatingActionButton( + text = { Text(stringResource(R.string.cancel)) }, + onClick = cancelCallback, icon = { + Icon( + Icons.Outlined.Cancel, + contentDescription = stringResource(R.string.cancel_download) + ) + }, modifier = Modifier.padding(vertical = 12.dp) + ) + } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt index 8a18c669..3fd3ed04 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt @@ -39,7 +39,7 @@ fun SpotifyPageBinder( SpotifyDataType.PLAYLIST -> { val playlist = data as? Playlist playlist?.let { - PlaylistViewPage(playlist, modifier) + PlaylistViewPage(playlist, modifier, trackDownloadCallback) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index 7f95e02b..37199662 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -1,7 +1,9 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize @@ -10,6 +12,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,13 +34,14 @@ import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.AsyncImageImpl import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.MarqueeText -import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage +import com.bobbyesp.spowlo.ui.components.songs.metadata_viewer.TrackComponent import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.dataStringToString @Composable fun PlaylistViewPage( data: Playlist, - modifier: Modifier + modifier: Modifier, + trackDownloadCallback: (String, String) -> Unit ) { val localConfig = LocalConfiguration.current @@ -102,11 +109,40 @@ fun PlaylistViewPage( modifier = Modifier.alpha(alpha = 0.8f) ) } - HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically) { + FilledTonalIconButton( + onClick = { + trackDownloadCallback(data.externalUrls.spotify!!, data.name) + }, + modifier = Modifier.size(48.dp), + ) { + Icon( + imageVector = Icons.Filled.Download, + contentDescription = "Download full playlist icon", + modifier = Modifier + .weight(1f) + .padding(14.dp) + ) + } + + } + HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) Column( modifier = Modifier.fillMaxWidth() ) { - NotImplementedPage() + //for every track in the playlist, show the track name and the artist name + data.tracks.items.forEach { track -> + val actualTrack = track.track?.asTrack + val taskName = StringBuilder().append(actualTrack?.name).append(" - ").append(actualTrack?.artists?.joinToString(", ") { it.name }).toString() + TrackComponent( + contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + songName = actualTrack?.name ?: App.context.getString(R.string.unknown), + artists = actualTrack?.artists?.joinToString(", ") { it.name } ?: "", + spotifyUrl = actualTrack?.externalUrls?.spotify!!, + isExplicit = actualTrack.explicit, + onClick = { trackDownloadCallback(actualTrack.externalUrls.spotify!!, taskName) } + ) + } } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index ac437265..6729ad59 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -123,7 +123,8 @@ fun TrackPage( artists = data.artists.joinToString(", ") { it.name }, spotifyUrl = data.externalUrls.spotify!!, isExplicit = data.explicit, - onClick = { trackDownloadCallback(data.externalUrls.spotify!!, taskName) }) + onClick = { trackDownloadCallback(data.externalUrls.spotify!!, taskName) } + ) } Spacer(modifier = Modifier.padding(vertical = 8.dp)) Column(modifier = Modifier.fillMaxWidth()) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/CookiesSettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/CookiesSettingsPage.kt index 1f800b71..34bef861 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/CookiesSettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/CookiesSettingsPage.kt @@ -213,9 +213,6 @@ fun CookieProfilePage( } } -@OptIn( - ExperimentalMaterial3Api::class -) @Composable fun CookieGeneratorDialog( cookiesViewModel: CookiesSettingsViewModel = viewModel(), @@ -253,7 +250,8 @@ fun CookieGeneratorDialog( TextButtonWithIcon( onClick = { navigateToCookieGeneratorPage() }, icon = Icons.Outlined.GeneratingTokens, - text = stringResource(id = R.string.generate_new_cookies) + text = stringResource(id = R.string.generate_new_cookies), + enabled = url.isNotEmpty() ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt index e93c5210..f2ec90cc 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt @@ -30,7 +30,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback @@ -46,9 +45,10 @@ import com.bobbyesp.spowlo.ui.common.intState import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.LargeTopAppBar -import com.bobbyesp.spowlo.ui.components.PreferenceItem import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle -import com.bobbyesp.spowlo.ui.components.PreferenceSwitch +import com.bobbyesp.spowlo.ui.components.settings.ElevatedSettingsCard +import com.bobbyesp.spowlo.ui.components.settings.SettingsItemNew +import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitch import com.bobbyesp.spowlo.utils.DEBUG import com.bobbyesp.spowlo.utils.DONT_FILTER_RESULTS import com.bobbyesp.spowlo.utils.GEO_BYPASS @@ -71,9 +71,9 @@ fun GeneralSettingsPage( val scope = rememberCoroutineScope() val hapticFeedback = LocalHapticFeedback.current - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - rememberTopAppBarState(), - canScroll = { true }) + val scrollBehavior = + TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState(), + canScroll = { true }) var displayErrorReport by DEBUG.booleanState @@ -134,113 +134,82 @@ fun GeneralSettingsPage( }, content = { LazyColumn( - modifier = Modifier.padding(it) + modifier = Modifier + .padding(it) + .padding(horizontal = 20.dp, vertical = 10.dp) ) { item { - PreferenceItem( - title = stringResource(id = R.string.spotdl_version), - description = spotDLVersion, - icon = Icons.Outlined.Info, - onClick = { - }, - onClickLabel = stringResource(id = R.string.update), - onLongClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + ElevatedSettingsCard { + SettingsItemNew( + onClick = { }, + title = { Text(text = stringResource(id = R.string.spotdl_version)) }, + icon = Icons.Outlined.Info, + description = { Text(text = spotDLVersion) }) - }, onLongClickLabel = stringResource(id = R.string.open_settings) - ) - } - item { - PreferenceSwitch( - title = stringResource(R.string.print_details), - description = stringResource(R.string.print_details_desc), - icon = if (displayErrorReport) Icons.Outlined.Print else Icons.Outlined.PrintDisabled, - enabled = true, - onClick = { - displayErrorReport = !displayErrorReport - PreferencesUtil.updateValue(DEBUG, displayErrorReport) - }, - isChecked = displayErrorReport - ) + SettingsSwitch( + onCheckedChange = { + displayErrorReport = !displayErrorReport + PreferencesUtil.updateValue(DEBUG, displayErrorReport) + }, + checked = displayErrorReport, + title = { + Text(text = stringResource(R.string.print_details)) + }, + icon = if (displayErrorReport) Icons.Outlined.Print else Icons.Outlined.PrintDisabled, + description = { Text(text = stringResource(R.string.print_details_desc)) }, + ) + } } item { PreferenceSubtitle(text = stringResource(id = R.string.library_settings)) } item { - PreferenceSwitch( - title = stringResource(id = R.string.use_cache), - description = stringResource(id = R.string.use_cache_desc), - icon = Icons.Outlined.Cached, - onClick = { - scope.launch { + ElevatedSettingsCard { + SettingsSwitch( + onCheckedChange = { useCache = !useCache PreferencesUtil.updateValue(USE_CACHING, useCache) - } - }, - isChecked = useCache - ) - } - item { - PreferenceSwitch( - title = stringResource(id = R.string.geo_bypass), - description = stringResource(id = R.string.use_geobypass_desc), - icon = Icons.Outlined.MyLocation, - onClick = { - scope.launch { + }, + checked = useCache, + title = { + Text(text = stringResource(id = R.string.use_cache)) + }, + icon = Icons.Outlined.Cached, + description = { Text(text = stringResource(id = R.string.use_cache_desc)) }, + ) + + SettingsSwitch( + onCheckedChange = { useGeobypass = !useGeobypass PreferencesUtil.updateValue(GEO_BYPASS, useGeobypass) - } - }, - isChecked = useGeobypass - ) - } - item { - PreferenceSwitch( - title = stringResource(id = R.string.dont_filter_results), - description = stringResource(id = R.string.dont_filter_results_desc), - icon = Icons.Outlined.Filter, - onClick = { - scope.launch { + }, + checked = useGeobypass, + title = { + Text(text = stringResource(id = R.string.geo_bypass)) + }, + icon = Icons.Outlined.MyLocation, + description = { Text(text = stringResource(id = R.string.use_geobypass_desc)) }, + ) + + SettingsSwitch( + onCheckedChange = { dontFilter = !dontFilter PreferencesUtil.updateValue(DONT_FILTER_RESULTS, dontFilter) - } - }, - isChecked = dontFilter - ) + }, + checked = dontFilter, + title = { + Text(text = stringResource(id = R.string.dont_filter_results)) + }, + icon = Icons.Outlined.Filter, + description = { Text(text = stringResource(id = R.string.dont_filter_results_desc)) }, + ) + } } item { - HorizontalDivider() + HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) } item { - /*PreferenceItem( - title = stringResource(id = R.string.threads_number), - description = stringResource(id = R.string.threads_number_desc), - icon = Icons.Outlined.Settings, - onClick = { - scope.launch { - val result = MaterialDialog(context).show { - title(text = stringResource(id = R.string.threads_number)) - message(text = stringResource(id = R.string.threads_number_desc)) - input( - hint = stringResource(id = R.string.threads_number), - prefill = threadsNumber.toString(), - inputType = InputType.TYPE_CLASS_NUMBER - ) { _, text -> - threadsNumber = text.toString().toInt() - PreferencesUtil.updateValue(THREADS_NUMBER, threadsNumber) - } - positiveButton(text = stringResource(id = R.string.ok)) - negativeButton(text = stringResource(id = R.string.cancel)) - } - } - }, - onClickLabel = stringResource(id = R.string.update), - onLongClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - - }, onLongClickLabel = stringResource(id = R.string.open_settings) - )*/ //threads number item with a slicer Column( modifier = Modifier.fillMaxWidth() @@ -275,8 +244,7 @@ fun GeneralSettingsPage( Text( text = stringResource(id = R.string.threads_number_desc), modifier = Modifier.padding( - vertical = 12.dp, - horizontal = 16.dp + vertical = 12.dp, horizontal = 16.dp ), style = MaterialTheme.typography.bodyMedium.copy( color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index 4fe73d51..64b5d2f0 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -411,12 +411,12 @@ object DownloaderUtil { onProcessStarted() onTaskStarted(url, name) kotlin.runCatching { - val response = SpotDL.getInstance().execute(request, taskId) { progress, _, text -> - Log.d(TAG, "executeParallelDownload: $progress $text") + val response = SpotDL.getInstance().execute(request = request, processId = taskId, forceProcessDestroy = true, callback = { progress, _, text -> + //Log.d(TAG, "executeParallelDownload: $progress $text") Downloader.updateTaskOutput( url = url, line = text, progress = progress ) - } + } ) //clear all the lines that contains a "…" on it val finalResponse = removeDuplicateLines(clearLinesWithEllipsis(response.output)) onTaskEnded(url, finalResponse) @@ -426,7 +426,7 @@ object DownloaderUtil { if (it is SpotDL.CanceledException) return@onFailure it.message.run { if (isNullOrEmpty()) onTaskEnded(url) - else onTaskError(this, url, taskId) + else onTaskError(this, url) } } onProcessEnded() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4858375..999cc8d8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -306,4 +306,6 @@ Spowlo is downloading Executing parallel download Followers + Here will appear all the downloads that you make from the searcher page. + The log has been copied to the clipboard \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1fcc77d6..5973ca4d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ androidxHiltNavigationCompose = "1.0.0" androidxTestExt = "1.1.5" -spotdlAndroidVersion = "074aea43bb" +spotdlAndroidVersion = "dd296e76cc" spotifyApiKotlinVersion = "3.8.8" crashHandlerVersion = "2.0.2" From c3db8f24f1e4f2aea41c3cf2108cb579322c30d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 1 Apr 2023 00:01:53 +0200 Subject: [PATCH 35/42] beat & feat: Changed page headers to bold, added a new Downloader settings page and improved notifications --- .../java/com/bobbyesp/spowlo/Downloader.kt | 55 ++++- .../com/bobbyesp/spowlo/ui/common/Route.kt | 1 + .../spowlo/ui/components/SettingItem.kt | 6 +- .../bottomsheets/DownloaderBottomSheet.kt | 2 +- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 21 +- .../ui/pages/downloader/DownloaderPage.kt | 3 +- .../history/DownloadHistoryBottomDrawer.kt | 10 +- .../spowlo/ui/pages/searcher/SearcherPage.kt | 38 ++- .../spowlo/ui/pages/settings/SettingsPage.kt | 114 +++++++-- .../ui/pages/settings/about/AboutPage.kt | 2 + .../appearance/AppThemePreferencesPage.kt | 3 +- .../settings/appearance/AppearancePage.kt | 3 +- .../pages/settings/appearance/LanguagePage.kt | 3 +- .../settings/cookies/CookiesSettingsPage.kt | 3 +- .../ui/pages/settings/cookies/WebViewPage.kt | 3 +- .../directories/DownloadsDirectoriesPage.kt | 18 +- .../documentation/DocumentationPage.kt | 7 +- .../downloader/DownloaderSettingsPage.kt | 226 ++++++++++++++++++ .../settings/format/SettingsFormatsPage.kt | 3 +- .../settings/general/GeneralSettingsPage.kt | 159 ++++-------- .../settings/spotify/SpotifySettingsPage.kt | 53 +++- .../ui/pages/settings/updater/UpdaterPage.kt | 3 +- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 30 ++- .../bobbyesp/spowlo/utils/PreferencesUtil.kt | 3 + app/src/main/res/values/strings.xml | 11 +- 25 files changed, 562 insertions(+), 218 deletions(-) create mode 100644 app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/downloader/DownloaderSettingsPage.kt diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index e9cbb38f..21e1d0ad 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -200,30 +200,69 @@ object Downloader { taskName = name ).run { mutableTaskList.put(this.toKey(), this) + + val key = makeKey(url, url.reversed()) + NotificationsUtil.notifyProgress( + name + " - " + context.getString(R.string.parallel_download), + notificationId = key.toNotificationId(), + progress = (state as DownloadTask.State.Running).progress.toInt(), + text = currentLine + ) } - fun updateTaskOutput(url: String, line: String, progress: Float) { + fun updateTaskOutput(url: String, line: String, progress: Float, isPlaylist: Boolean = false) { val key = makeKey(url, url.reversed()) val oldValue = mutableTaskList[key] ?: return val newValue = oldValue.run { if (currentLine == line || line.containsEllipsis() || consoleOutput.contains(line)) return - copy( - consoleOutput = consoleOutput + line + "\n", - currentLine = line, - state = DownloadTask.State.Running(progress) - ) + when(isPlaylist) { + true -> { + copy( + consoleOutput = consoleOutput + line + "\n", + currentLine = line, + state = DownloadTask.State.Running( + if (line.contains("Total")) { + getProgress(line) + } else { + (state as DownloadTask.State.Running).progress + } + ) + ) + + } + false -> { + copy( + consoleOutput = consoleOutput + line + "\n", + currentLine = line, + state = DownloadTask.State.Running(progress) + ) + } + } } mutableTaskList[key] = newValue } + private fun getProgress(line: String): Float{ + val PERCENT: Float + //Get the two numbers before an % in the line + val regex = Regex("(\\d+)%") + val matchResult = regex.find(line) + //Log the result + ///if (BuildConfig.DEBUG) Log.d(TAG, "Progress: ${matchResult?.groupValues?.get(1)?.toFloat() ?: 0f}") + PERCENT = matchResult?.groupValues?.get(1)?.toFloat() ?: 0f + //divide percent by 100 to get a value between 0 and 1 + return PERCENT / 100f + } + fun onTaskEnded( url: String, - response: String? = null + response: String? = null, + notificationTitle : String? = null ) { val key = makeKey(url, url.reversed()) NotificationsUtil.finishNotification( notificationId = key.toNotificationId(), - title = key, + title = notificationTitle, text = context.getString(R.string.status_completed), ) mutableTaskList.run { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt index 31a900ab..064de0a4 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/common/Route.kt @@ -2,6 +2,7 @@ package com.bobbyesp.spowlo.ui.common object Route { + const val DOWNLOADER_SETTINGS = "downloader_settings" const val DOWNLOADER_SHEET = "downloader_sheet" const val NavGraph = "nav_graph" const val SearcherNavi = "searcher_navi" diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/SettingItem.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/SettingItem.kt index 899bd10c..f59faef5 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/SettingItem.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/SettingItem.kt @@ -14,17 +14,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @Composable -fun SettingTitle(text: String) { +fun SettingTitle(text: String, fontWeight: FontWeight = FontWeight.Normal) { Text( modifier = Modifier .padding(top = 32.dp) .padding(horizontal = 20.dp, vertical = 16.dp), text = text, - style = MaterialTheme.typography.displaySmall + style = MaterialTheme.typography.displaySmall, + fontWeight = fontWeight, ) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt index bd7a3cef..b046c111 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt @@ -273,7 +273,7 @@ fun DownloaderBottomSheet( Column( modifier = Modifier.fillMaxWidth().padding(6.dp) ) { - DrawerSheetSubtitle(text = stringResource(id = R.string.general_settings)) + DrawerSheetSubtitle(text = stringResource(id = R.string.general)) Row( modifier = Modifier .horizontalScroll(rememberScrollState()) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index b81efb9d..6ea3e936 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -88,6 +88,7 @@ import com.bobbyesp.spowlo.ui.pages.settings.cookies.CookiesSettingsViewModel import com.bobbyesp.spowlo.ui.pages.settings.cookies.WebViewPage import com.bobbyesp.spowlo.ui.pages.settings.directories.DownloadsDirectoriesPage import com.bobbyesp.spowlo.ui.pages.settings.documentation.DocumentationPage +import com.bobbyesp.spowlo.ui.pages.settings.downloader.DownloaderSettingsPage import com.bobbyesp.spowlo.ui.pages.settings.format.AudioQualityDialog import com.bobbyesp.spowlo.ui.pages.settings.format.SettingsFormatsPage import com.bobbyesp.spowlo.ui.pages.settings.general.GeneralSettingsPage @@ -343,6 +344,11 @@ fun InitialEntry( onBackPressed() } } + animatedComposable(Route.DOWNLOADER_SETTINGS) { + DownloaderSettingsPage { + onBackPressed() + } + } slideInVerticallyComposable(Route.PLAYLIST_METADATA_PAGE) { PlaylistMetadataPage( onBackPressed, @@ -489,6 +495,7 @@ fun InitialEntry( onBackPressed, id = id, type = type, + playlistPageViewModel = playlistPageViewModel, ) } } @@ -526,22 +533,12 @@ fun InitialEntry( }.onFailure { it.printStackTrace() ToastUtil.makeToastSuspend(context.getString(R.string.spotify_api_error)) - }.onSuccess { - val req = SpotifyApiRequests.provideSearchAllTypes("Faded Alan Walker") - Log.d("InitialEntry", "Name:" + req.tracks!![0].name) - Log.d("InitialEntry", "Artist:" + req.tracks!![0].artists[0].name) - Log.d("InitialEntry", "Album:" + req.tracks!![0].album.name) - Log.d("InitialEntry", "Album Image:" + req.tracks!![0].album.images[0].url) - Log.d("InitialEntry", "Duration:" + req.tracks!![0].durationMs) - Log.d("InitialEntry", "Popularity:" + req.tracks!![0].popularity) - Log.d("InitialEntry", "-------------------------------------------") - Log.d("InitialEntry", "Full response: $req") } } LaunchedEffect(Unit) { - if (PreferencesUtil.isNetworkAvailableForDownload()) launch(Dispatchers.IO) { + if (PreferencesUtil.isNetworkAvailable()) launch(Dispatchers.IO) { runCatching { UpdateUtil.checkForUpdate()?.let { latestRelease = it @@ -559,7 +556,7 @@ fun InitialEntry( LaunchedEffect(Unit) { Log.d(TAG, "InitialEntry: Checking for mod updates") - if (PreferencesUtil.isNetworkAvailableForDownload()) ModsDownloaderAPI.getAPIResponse() + if (PreferencesUtil.isNetworkAvailable()) ModsDownloaderAPI.getAPIResponse() .onSuccess { Log.d(TAG, "InitialEntry: Mods API call success") modsDownloaderViewModel.updateApiResponse(it) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index b0d2dbce..d03e2a76 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -199,7 +199,8 @@ fun DownloaderPage( onUrlChanged = { url -> downloaderViewModel.updateUrl(url) }) {} with(viewState) { - DownloaderSettingsDialog(useDialog = useDialog, + DownloaderSettingsDialog( + useDialog = useDialog, dialogState = showDownloadSettingDialog, drawerState = drawerState, confirm = { checkPermissionOrDownload() }, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadHistoryBottomDrawer.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadHistoryBottomDrawer.kt index 598e99e6..34ddcd24 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadHistoryBottomDrawer.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadHistoryBottomDrawer.kt @@ -3,6 +3,7 @@ package com.bobbyesp.spowlo.ui.pages.history import android.content.Intent import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -144,9 +145,9 @@ fun DownloadHistoryBottomDrawerImpl( val clipboardManager = LocalClipboardManager.current val context = LocalContext.current - BottomDrawer(drawerState = drawerState, sheetContent = { + BottomDrawer(modifier = Modifier.animateContentSize(),drawerState = drawerState, sheetContent = { AnimatedVisibility(visible = !showDeleteInfo) { - Column(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.fillMaxWidth().animateContentSize()) { Row( modifier = Modifier .fillMaxWidth(), @@ -232,7 +233,8 @@ fun DownloadHistoryBottomDrawerImpl( Column( modifier = Modifier .fillMaxWidth() - .padding(6.dp), + .padding(6.dp) + .animateContentSize(), horizontalAlignment = Alignment.Start ) { Row( @@ -278,7 +280,7 @@ fun DownloadHistoryBottomDrawerImpl( modifier = Modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()) - .padding(top = 24.dp), + .padding(top = 24.dp).animateContentSize(), ) { OutlinedButtonWithIcon( modifier = Modifier diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 5c79dba7..83a2a8a8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -47,6 +47,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.Route +import com.bobbyesp.spowlo.ui.components.AutoResizableText import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.songs.search_feat.SearchingSongComponent import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfDataToString @@ -66,7 +67,7 @@ fun SearcherPage( .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { - SearcherPageImpl( + SearcherPageImpl( viewState = viewState, onValueChange = { query -> searcherPageViewModel.updateSearchText(query) @@ -112,24 +113,16 @@ fun SearcherPageImpl( Box( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.background) + .background(MaterialTheme.colorScheme.background).padding(horizontal = 16.dp), + contentAlignment = Alignment.Center ) { - Column( - modifier = Modifier.align(Alignment.Center), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(id = R.string.search), - modifier = Modifier.align( - Alignment.CenterHorizontally - ), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Bold - ) - } - + AutoResizableText( + text = stringResource(id = R.string.search), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) } + } } @@ -195,7 +188,7 @@ fun SearcherPageImpl( mutableListOf() //TODO: Add the filters. Pagination should be done in the future viewState.viewState.data.let { data -> data.albums?.items?.let {/* allItems.addAll(it)*/ } - data.artists?.items?.let { /*allItems.addAll(it) */} + data.artists?.items?.let { /*allItems.addAll(it) */ } data.playlists?.items?.let { allItems.addAll(it) } data.tracks?.items?.let { allItems.addAll(it) } data.episodes?.items?.let {/* @@ -250,7 +243,7 @@ fun SearcherPageImpl( songName = track.name, artists = artists.joinToString(", "), spotifyUrl = track.externalUrls.spotify ?: "", - onClick = { onItemClick(track.type ,track.id) }, + onClick = { onItemClick(track.type, track.id) }, type = typeOfDataToString( type = typeOfSpotifyDataType( track.type @@ -272,7 +265,12 @@ fun SearcherPageImpl( artists = playlist.owner.displayName ?: stringResource(R.string.unknown), spotifyUrl = playlist.externalUrls.spotify ?: "", - onClick = { onItemClick(playlist.type ,playlist.id) }, + onClick = { + onItemClick( + playlist.type, + playlist.id + ) + }, type = typeOfDataToString( type = typeOfSpotifyDataType( playlist.type diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt index 2517b2da..65b9279e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt @@ -23,6 +23,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Aod import androidx.compose.material.icons.filled.AudioFile import androidx.compose.material.icons.filled.Cookie +import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Help import androidx.compose.material.icons.filled.Info @@ -30,6 +31,7 @@ import androidx.compose.material.icons.filled.SettingsApplications import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.rounded.EnergySavingsLeaf import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -53,6 +55,7 @@ import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.PreferencesHintCard import com.bobbyesp.spowlo.ui.components.SettingTitle import com.bobbyesp.spowlo.ui.components.SmallTopAppBar +import com.bobbyesp.spowlo.ui.components.fraction import com.bobbyesp.spowlo.ui.components.settings.SettingsItemNew import com.bobbyesp.spowlo.ui.pages.settings.about.LocalAsset @@ -85,6 +88,16 @@ fun SettingsPage(navController: NavController) { topBar = { SmallTopAppBar( titleText = stringResource(id = R.string.settings), + title = { + Text( + text = stringResource(id = R.string.settings), + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface.copy( + alpha = fraction(scrollBehavior.state.overlappedFraction) + ), + maxLines = 1 + ) + }, navigationIcon = { BackButton { navController.popBackStack() } }, scrollBehavior = scrollBehavior ) @@ -95,7 +108,7 @@ fun SettingsPage(navController: NavController) { contentPadding = PaddingValues(horizontal = 16.dp) ) { item { - SettingTitle(text = stringResource(id = R.string.settings)) + SettingTitle(text = stringResource(id = R.string.settings), fontWeight = FontWeight.Bold) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (context.packageManager.queryIntentActivities( @@ -124,22 +137,49 @@ fun SettingsPage(navController: NavController) { } item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.general_settings), fontWeight = FontWeight.Bold) }, + title = { + Text( + text = stringResource(id = R.string.general), + fontWeight = FontWeight.Bold + ) + }, description = { Text(text = stringResource(id = R.string.general_settings_desc)) }, icon = Icons.Filled.SettingsApplications, - onClick = { - navController.navigate(Route.GENERAL_DOWNLOAD_PREFERENCES) { - launchSingleTop = true - } - }, + onClick = { + navController.navigate(Route.GENERAL_DOWNLOAD_PREFERENCES) { + launchSingleTop = true + } + }, addTonalElevation = true, modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)), highlightIcon = true ) } + item { + SettingsItemNew(onClick = { + navController.navigate(Route.DOWNLOADER_SETTINGS) { + launchSingleTop = true + } + }, title = { + Text( + text = stringResource(id = R.string.downloader), + fontWeight = FontWeight.Bold + ) + }, description = { + Text(text = stringResource(id = R.string.downloader_settings_desc)) + }, icon = Icons.Filled.Download, + highlightIcon = true + ) + + } item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.spotify_settings), fontWeight = FontWeight.Bold) }, + title = { + Text( + text = stringResource(id = R.string.spotify_settings), + fontWeight = FontWeight.Bold + ) + }, description = { Text(text = stringResource(id = R.string.spotify_settings_desc)) }, icon = LocalAsset(id = R.drawable.spotify_logo), onClick = { @@ -154,7 +194,12 @@ fun SettingsPage(navController: NavController) { item { //new settings item for download directory SettingsItemNew( - title = { Text(text = stringResource(id = R.string.download_directory), fontWeight = FontWeight.Bold) }, + title = { + Text( + text = stringResource(id = R.string.download_directory), + fontWeight = FontWeight.Bold + ) + }, description = { Text(text = stringResource(id = R.string.download_directory_desc)) }, icon = Icons.Filled.Folder, onClick = { @@ -168,7 +213,12 @@ fun SettingsPage(navController: NavController) { } item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.format), fontWeight = FontWeight.Bold) }, + title = { + Text( + text = stringResource(id = R.string.format), + fontWeight = FontWeight.Bold + ) + }, description = { Text(text = stringResource(id = R.string.format_settings_desc)) }, icon = Icons.Filled.AudioFile, onClick = { @@ -183,7 +233,12 @@ fun SettingsPage(navController: NavController) { item { //rewrite this with new settings item SettingsItemNew( - title = { Text(text = stringResource(id = R.string.appearance), fontWeight = FontWeight.Bold) }, + title = { + Text( + text = stringResource(id = R.string.appearance), + fontWeight = FontWeight.Bold + ) + }, description = { Text(text = stringResource(id = R.string.appearance_settings)) }, icon = Icons.Filled.Aod, onClick = { @@ -198,7 +253,12 @@ fun SettingsPage(navController: NavController) { item { //Cookies page SettingsItemNew( - title = { Text(text = stringResource(id = R.string.cookies), fontWeight = FontWeight.Bold) }, + title = { + Text( + text = stringResource(id = R.string.cookies), + fontWeight = FontWeight.Bold + ) + }, description = { Text(text = stringResource(id = R.string.cookies_desc)) }, icon = Icons.Filled.Cookie, onClick = { @@ -213,7 +273,12 @@ fun SettingsPage(navController: NavController) { item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.documentation), fontWeight = FontWeight.Bold) }, + title = { + Text( + text = stringResource(id = R.string.documentation), + fontWeight = FontWeight.Bold + ) + }, description = { Text(text = stringResource(id = R.string.documentation_desc)) }, icon = Icons.Filled.Help, onClick = { @@ -226,9 +291,14 @@ fun SettingsPage(navController: NavController) { ) } - item{ + item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.updates_channels), fontWeight = FontWeight.Bold) }, + title = { + Text( + text = stringResource(id = R.string.updates_channels), + fontWeight = FontWeight.Bold + ) + }, description = { Text(text = stringResource(id = R.string.updates_channels_desc)) }, icon = Icons.Filled.Update, onClick = { @@ -243,7 +313,12 @@ fun SettingsPage(navController: NavController) { item { SettingsItemNew( - title = { Text(text = stringResource(id = R.string.about), fontWeight = FontWeight.Bold) }, + title = { + Text( + text = stringResource(id = R.string.about), + fontWeight = FontWeight.Bold + ) + }, description = { Text(text = stringResource(id = R.string.about_page)) }, icon = Icons.Filled.Info, onClick = { @@ -253,7 +328,12 @@ fun SettingsPage(navController: NavController) { }, addTonalElevation = true, highlightIcon = true, - modifier = Modifier.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)) + modifier = Modifier.clip( + RoundedCornerShape( + bottomStart = 8.dp, + bottomEnd = 8.dp + ) + ) ) } item { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/about/AboutPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/about/AboutPage.kt index 0b0375e0..ba970fd2 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/about/AboutPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/about/AboutPage.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.App.Companion.packageInfo @@ -69,6 +70,7 @@ fun AboutPage(onBackPressed: () -> Unit) { Text( modifier = Modifier, text = stringResource(id = R.string.about), + fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppThemePreferencesPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppThemePreferencesPage.kt index cffe1abe..481dfbe1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppThemePreferencesPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppThemePreferencesPage.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.LocalDarkTheme @@ -45,7 +46,7 @@ fun AppThemePreferencesPage(onBackPressed: () -> Unit) { title = { Text( modifier = Modifier.padding(start = 8.dp), - text = stringResource(R.string.dark_theme), + text = stringResource(R.string.dark_theme), fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton() { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt index 6cbf6b65..c18775e0 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.bobbyesp.library.dto.Song @@ -132,7 +133,7 @@ fun AppearancePage( LargeTopAppBar(title = { Text( modifier = Modifier, - text = stringResource(id = R.string.display), + text = stringResource(id = R.string.display), fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/LanguagePage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/LanguagePage.kt index 9f96a344..8c10dd31 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/LanguagePage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/LanguagePage.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.MainActivity import com.bobbyesp.spowlo.R @@ -63,7 +64,7 @@ fun LanguagePage(onBackPressed: () -> Unit) { title = { Text( modifier = Modifier, - text = stringResource(id = R.string.language), + text = stringResource(id = R.string.language), fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/CookiesSettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/CookiesSettingsPage.kt index 34bef861..602bc944 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/CookiesSettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/CookiesSettingsPage.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.LineBreak import androidx.compose.ui.unit.dp @@ -109,7 +110,7 @@ fun CookieProfilePage( LargeTopAppBar(title = { Text( modifier = Modifier, - text = stringResource(id = R.string.cookies), + text = stringResource(id = R.string.cookies), fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/WebViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/WebViewPage.kt index aef61b01..27c40889 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/WebViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/cookies/WebViewPage.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.utils.connectWithDelimiter @@ -93,7 +94,7 @@ fun WebViewPage( Scaffold(modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( - title = { Text(webViewState.pageTitle.toString(), maxLines = 1) }, + title = { Text(webViewState.pageTitle.toString(), maxLines = 1, fontWeight = FontWeight.Bold) }, navigationIcon = { IconButton( onClick = { onDismissRequest() }) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/directories/DownloadsDirectoriesPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/directories/DownloadsDirectoriesPage.kt index f8e5aed0..0bd75add 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/directories/DownloadsDirectoriesPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/directories/DownloadsDirectoriesPage.kt @@ -27,12 +27,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import com.bobbyesp.spowlo.utils.CUSTOM_PATH -import com.bobbyesp.spowlo.utils.PreferencesUtil -import com.bobbyesp.spowlo.utils.SUBDIRECTORY import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -40,7 +34,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.R @@ -49,14 +47,16 @@ import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.LargeTopAppBar import com.bobbyesp.spowlo.ui.components.PreferenceInfo import com.bobbyesp.spowlo.ui.components.PreferenceItem -import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle import com.bobbyesp.spowlo.ui.components.PreferenceSwitchWithDivider import com.bobbyesp.spowlo.ui.components.PreferencesHintCard import com.bobbyesp.spowlo.utils.CUSTOM_COMMAND +import com.bobbyesp.spowlo.utils.CUSTOM_PATH import com.bobbyesp.spowlo.utils.FilesUtil +import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.PreferencesUtil.getString import com.bobbyesp.spowlo.utils.SDCARD_DOWNLOAD import com.bobbyesp.spowlo.utils.SDCARD_URI +import com.bobbyesp.spowlo.utils.SUBDIRECTORY import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState @@ -166,7 +166,7 @@ fun DownloadsDirectoriesPage( title = { Text( modifier = Modifier, - text = stringResource(id = R.string.download_directory), + text = stringResource(id = R.string.download_directory), fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { @@ -200,7 +200,7 @@ fun DownloadsDirectoriesPage( } item { Text( - text = stringResource(id = R.string.general_settings), + text = stringResource(id = R.string.general), modifier = Modifier .fillMaxWidth() .padding(start = 18.dp, top = 12.dp, bottom = 4.dp), diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/documentation/DocumentationPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/documentation/DocumentationPage.kt index 3655b632..608c38ea 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/documentation/DocumentationPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/documentation/DocumentationPage.kt @@ -11,19 +11,16 @@ import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import androidx.navigation.NavController import com.bobbyesp.spowlo.R -import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.InlineEnterItem import com.bobbyesp.spowlo.ui.components.LargeTopAppBar import com.bobbyesp.spowlo.ui.components.PreferenceInfo -import com.bobbyesp.spowlo.utils.FilesUtil.inputStreamToString @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -45,7 +42,7 @@ fun DocumentationPage( title = { Text( modifier = Modifier, - text = stringResource(id = R.string.documentation) + text = stringResource(id = R.string.documentation), fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/downloader/DownloaderSettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/downloader/DownloaderSettingsPage.kt new file mode 100644 index 00000000..c881ec92 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/downloader/DownloaderSettingsPage.kt @@ -0,0 +1,226 @@ +package com.bobbyesp.spowlo.ui.pages.settings.downloader + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Cached +import androidx.compose.material.icons.outlined.Filter +import androidx.compose.material.icons.outlined.MyLocation +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.common.intState +import com.bobbyesp.spowlo.ui.components.BackButton +import com.bobbyesp.spowlo.ui.components.LargeTopAppBar +import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle +import com.bobbyesp.spowlo.ui.components.settings.ElevatedSettingsCard +import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitch +import com.bobbyesp.spowlo.utils.DONT_FILTER_RESULTS +import com.bobbyesp.spowlo.utils.GEO_BYPASS +import com.bobbyesp.spowlo.utils.PreferencesUtil +import com.bobbyesp.spowlo.utils.PreferencesUtil.updateInt +import com.bobbyesp.spowlo.utils.THREADS +import com.bobbyesp.spowlo.utils.USE_CACHING +import kotlinx.coroutines.DelicateCoroutinesApi + +@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class) +@Composable +fun DownloaderSettingsPage( + onBackPressed: () -> Unit, +) { + + var threadsNumber = THREADS.intState + + var useCache by remember { + mutableStateOf( + PreferencesUtil.getValue(USE_CACHING) + ) + } + + var dontFilter by remember { + mutableStateOf( + PreferencesUtil.getValue(DONT_FILTER_RESULTS) + ) + } + + var useGeobypass by remember { + mutableStateOf( + PreferencesUtil.getValue(GEO_BYPASS) + ) + } + + val scrollBehavior = + TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState(), + canScroll = { true }) + + Scaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + LargeTopAppBar( + title = { + Text( + text = stringResource(id = R.string.downloader), + fontWeight = FontWeight.Bold + ) + }, + navigationIcon = { + BackButton { onBackPressed() } + }, + scrollBehavior = scrollBehavior + ) + }, + content = { + LazyColumn( + modifier = Modifier + .padding(it) + .padding(horizontal = 20.dp, vertical = 10.dp) + ) { + item { + PreferenceSubtitle(text = stringResource(id = R.string.general)) + } + item { + ElevatedSettingsCard { + SettingsSwitch( + onCheckedChange = { + useCache = !useCache + PreferencesUtil.updateValue(USE_CACHING, useCache) + }, + checked = useCache, + title = { + Text( + text = stringResource(id = R.string.use_cache), + fontWeight = FontWeight.Bold + ) + }, + icon = Icons.Outlined.Cached, + description = { Text(text = stringResource(id = R.string.use_cache_desc)) }, + ) + } + } + item { + PreferenceSubtitle(text = stringResource(id = R.string.experimental_features)) + } + item { + ElevatedSettingsCard { + SettingsSwitch( + onCheckedChange = { + useGeobypass = !useGeobypass + PreferencesUtil.updateValue(GEO_BYPASS, useGeobypass) + }, + checked = useGeobypass, + title = { + Text( + text = stringResource(id = R.string.geo_bypass), + fontWeight = FontWeight.Bold + ) + }, + icon = Icons.Outlined.MyLocation, + description = { Text(text = stringResource(id = R.string.use_geobypass_desc)) }, + ) + + SettingsSwitch( + onCheckedChange = { + dontFilter = !dontFilter + PreferencesUtil.updateValue(DONT_FILTER_RESULTS, dontFilter) + }, + checked = dontFilter, + title = { + Text( + text = stringResource(id = R.string.dont_filter_results), + fontWeight = FontWeight.Bold + ) + }, + icon = Icons.Outlined.Filter, + description = { Text(text = stringResource(id = R.string.dont_filter_results_desc)) }, + ) + } + } + item { + PreferenceSubtitle(text = stringResource(id = R.string.advanced_features)) + } + item { + ElevatedSettingsCard { + //threads number item with a slicer + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.threads), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(start = 16.dp, top = 16.dp) + .weight(1f) + ) + Text( + text = stringResource(id = R.string.threads_number) + ": " + threadsNumber.value.toString(), + style = MaterialTheme.typography.labelLarge.copy( + color = MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.6f + ) + ), + modifier = Modifier.padding(end = 16.dp, top = 16.dp) + ) + } + Text( + text = stringResource(id = R.string.threads_number_desc), + modifier = Modifier.padding( + vertical = 12.dp, horizontal = 16.dp + ), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + ) + + } + } + Slider( + value = threadsNumber.value.toFloat(), + onValueChange = { + threadsNumber.value = it.toInt() + THREADS.updateInt(it.toInt()) + }, + valueRange = 1f..10f, + steps = 9, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt index 35586eb5..2d3be8ba 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.LargeTopAppBar @@ -57,7 +58,7 @@ fun SettingsFormatsPage(onBackPressed: () -> Unit) { title = { Text( modifier = Modifier, - text = stringResource(id = R.string.format), + text = stringResource(id = R.string.format), fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt index f2ec90cc..0bbc3467 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt @@ -1,23 +1,17 @@ package com.bobbyesp.spowlo.ui.pages.settings.general -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Cached -import androidx.compose.material.icons.outlined.Filter import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.MyLocation +import androidx.compose.material.icons.outlined.NotificationsActive +import androidx.compose.material.icons.outlined.NotificationsOff import androidx.compose.material.icons.outlined.Print import androidx.compose.material.icons.outlined.PrintDisabled import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState @@ -28,8 +22,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback @@ -43,7 +37,6 @@ import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.booleanState import com.bobbyesp.spowlo.ui.common.intState import com.bobbyesp.spowlo.ui.components.BackButton -import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.LargeTopAppBar import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle import com.bobbyesp.spowlo.ui.components.settings.ElevatedSettingsCard @@ -52,10 +45,9 @@ import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitch import com.bobbyesp.spowlo.utils.DEBUG import com.bobbyesp.spowlo.utils.DONT_FILTER_RESULTS import com.bobbyesp.spowlo.utils.GEO_BYPASS +import com.bobbyesp.spowlo.utils.NOTIFICATION import com.bobbyesp.spowlo.utils.PreferencesUtil -import com.bobbyesp.spowlo.utils.PreferencesUtil.updateInt import com.bobbyesp.spowlo.utils.THREADS -import com.bobbyesp.spowlo.utils.USE_CACHING import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -77,9 +69,9 @@ fun GeneralSettingsPage( var displayErrorReport by DEBUG.booleanState - var useCache by remember { + var useNotifications by remember { mutableStateOf( - PreferencesUtil.getValue(USE_CACHING) + PreferencesUtil.getValue(NOTIFICATION) ) } @@ -120,12 +112,13 @@ fun GeneralSettingsPage( } } - Scaffold(modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + Scaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { LargeTopAppBar( - title = { Text(text = stringResource(id = R.string.general_settings)) }, + title = { Text(text = stringResource(id = R.string.general), fontWeight = FontWeight.Bold) }, navigationIcon = { BackButton { onBackPressed() } }, @@ -142,7 +135,12 @@ fun GeneralSettingsPage( ElevatedSettingsCard { SettingsItemNew( onClick = { }, - title = { Text(text = stringResource(id = R.string.spotdl_version)) }, + title = { + Text( + text = stringResource(id = R.string.spotdl_version), + fontWeight = FontWeight.Bold + ) + }, icon = Icons.Outlined.Info, description = { Text(text = spotDLVersion) }) @@ -153,7 +151,10 @@ fun GeneralSettingsPage( }, checked = displayErrorReport, title = { - Text(text = stringResource(R.string.print_details)) + Text( + text = stringResource(R.string.print_details), + fontWeight = FontWeight.Bold + ) }, icon = if (displayErrorReport) Icons.Outlined.Print else Icons.Outlined.PrintDisabled, description = { Text(text = stringResource(R.string.print_details_desc)) }, @@ -162,109 +163,29 @@ fun GeneralSettingsPage( } item { - PreferenceSubtitle(text = stringResource(id = R.string.library_settings)) + PreferenceSubtitle(text = stringResource(id = R.string.general_settings)) } item { - ElevatedSettingsCard { - SettingsSwitch( - onCheckedChange = { - useCache = !useCache - PreferencesUtil.updateValue(USE_CACHING, useCache) - }, - checked = useCache, - title = { - Text(text = stringResource(id = R.string.use_cache)) - }, - icon = Icons.Outlined.Cached, - description = { Text(text = stringResource(id = R.string.use_cache_desc)) }, - ) - - SettingsSwitch( - onCheckedChange = { - useGeobypass = !useGeobypass - PreferencesUtil.updateValue(GEO_BYPASS, useGeobypass) - }, - checked = useGeobypass, - title = { - Text(text = stringResource(id = R.string.geo_bypass)) - }, - icon = Icons.Outlined.MyLocation, - description = { Text(text = stringResource(id = R.string.use_geobypass_desc)) }, + SettingsSwitch( + onCheckedChange = { + useNotifications = !useNotifications + PreferencesUtil.updateValue(NOTIFICATION, useNotifications) + }, + checked = useNotifications, + title = { + Text( + text = stringResource(R.string.use_notifications), + fontWeight = FontWeight.Bold + ) + }, + icon = if(useNotifications) Icons.Outlined.NotificationsActive else Icons.Outlined.NotificationsOff, + description = { + Text(text = stringResource(R.string.use_notifications_desc)) + }, + modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)), ) - - SettingsSwitch( - onCheckedChange = { - dontFilter = !dontFilter - PreferencesUtil.updateValue(DONT_FILTER_RESULTS, dontFilter) - }, - checked = dontFilter, - title = { - Text(text = stringResource(id = R.string.dont_filter_results)) - }, - icon = Icons.Outlined.Filter, - description = { Text(text = stringResource(id = R.string.dont_filter_results_desc)) }, - ) - } } - item { - HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) - } - item { - //threads number item with a slicer - Column( - modifier = Modifier.fillMaxWidth() - ) { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.CenterStart - ) { - Column(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(id = R.string.threads), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - modifier = Modifier - .padding(start = 16.dp, top = 16.dp) - .weight(1f) - ) - Text( - text = stringResource(id = R.string.threads_number) + ": " + threadsNumber.value.toString(), - style = MaterialTheme.typography.labelLarge.copy( - color = MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.6f - ) - ), - modifier = Modifier.padding(end = 16.dp, top = 16.dp) - ) - } - Text( - text = stringResource(id = R.string.threads_number_desc), - modifier = Modifier.padding( - vertical = 12.dp, horizontal = 16.dp - ), - style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - ) - } - } - Slider( - value = threadsNumber.value.toFloat(), - onValueChange = { - threadsNumber.value = it.toInt() - THREADS.updateInt(it.toInt()) - }, - valueRange = 1f..10f, - steps = 9, - modifier = Modifier.padding(horizontal = 16.dp) - ) - } - } } }) } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt index 4b3b35a8..c67a9eef 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt @@ -4,15 +4,20 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.PermIdentity +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Divider 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.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.surfaceColorAtElevation @@ -24,6 +29,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.components.BackButton @@ -68,6 +74,7 @@ fun SpotifySettingsPage(onBackPressed: () -> Unit) { var showClientIdDialog by remember { mutableStateOf(false) } var showClientSecretDialog by remember { mutableStateOf(false) } + var showHelpDialog by remember { mutableStateOf(false) } Scaffold( modifier = Modifier @@ -78,13 +85,24 @@ fun SpotifySettingsPage(onBackPressed: () -> Unit) { title = { Text( modifier = Modifier, - text = stringResource(id = R.string.spotify_settings), + text = stringResource(id = R.string.spotify_settings), fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { onBackPressed() } - }, scrollBehavior = scrollBehavior + }, + scrollBehavior = scrollBehavior, + actions = { + IconButton(onClick = { + showHelpDialog = !showHelpDialog + }) { + Icon( + imageVector = Icons.Outlined.HelpOutline, + contentDescription = stringResource(R.string.how_does_it_work) + ) + } + } ) }, content = { LazyColumn( @@ -93,7 +111,7 @@ fun SpotifySettingsPage(onBackPressed: () -> Unit) { .padding(horizontal = 20.dp) ) { item { - PreferenceSubtitle(text = stringResource(id = R.string.general_settings)) + PreferenceSubtitle(text = stringResource(id = R.string.general)) } item { Card( @@ -165,4 +183,33 @@ fun SpotifySettingsPage(onBackPressed: () -> Unit) { showClientSecretDialog = !showClientSecretDialog } } + if (showHelpDialog) { + SpotifySettingsPageInfoDialog { + showHelpDialog = !showHelpDialog + } + } +} + +@Composable +fun SpotifySettingsPageInfoDialog(onDismiss: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(stringResource(id = R.string.spotify_api_info)) + }, + text = { + Text(stringResource(id = R.string.spotify_api_info_description)) + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(id = R.string.agree)) + } + }, + icon = { + Icon( + imageVector = Icons.Outlined.HelpOutline, + contentDescription = stringResource(id = R.string.how_does_it_work) + ) + } + ) } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/updater/UpdaterPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/updater/UpdaterPage.kt index c47dcd87..4af77ad9 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/updater/UpdaterPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/updater/UpdaterPage.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.booleanState @@ -77,7 +78,7 @@ fun UpdaterPage(onBackPressed: () -> Unit) { LargeTopAppBar(title = { Text( modifier = Modifier, - text = stringResource(id = R.string.auto_update), + text = stringResource(id = R.string.auto_update), fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index 64b5d2f0..d980d3b5 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -15,6 +15,7 @@ import com.bobbyesp.spowlo.Downloader.onProcessStarted import com.bobbyesp.spowlo.Downloader.onTaskEnded import com.bobbyesp.spowlo.Downloader.onTaskError import com.bobbyesp.spowlo.Downloader.onTaskStarted +import com.bobbyesp.spowlo.Downloader.toNotificationId import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.database.DownloadedSongInfo import com.bobbyesp.spowlo.ui.pages.settings.cookies.Cookie @@ -389,7 +390,7 @@ object DownloaderUtil { } } - fun createIntFromString(string: String): Int { + private fun createIntFromString(string: String): Int { var int = 0 for (i in string.indices) { int += string[i].code @@ -399,7 +400,7 @@ object DownloaderUtil { fun executeParallelDownload(url: String, name: String) { - val taskId = Downloader.makeKey(url = url, additionalString = url.reversed()) + val taskId = Downloader.makeKey(url, url.reversed()) ToastUtil.makeToastSuspend(context.getString(R.string.download_started_msg)) val pathBuilder = StringBuilder() @@ -408,18 +409,29 @@ object DownloaderUtil { addOption("--threads", downloadPreferences.threads.toString()) } + val isPlaylist = url.contains("playlist") + onProcessStarted() onTaskStarted(url, name) kotlin.runCatching { - val response = SpotDL.getInstance().execute(request = request, processId = taskId, forceProcessDestroy = true, callback = { progress, _, text -> - //Log.d(TAG, "executeParallelDownload: $progress $text") - Downloader.updateTaskOutput( - url = url, line = text, progress = progress - ) - } ) + val response = SpotDL.getInstance().execute( + request = request, + processId = taskId, + forceProcessDestroy = true, + callback = { progress, _, text -> + NotificationsUtil.notifyProgress( + name + " - " + context.getString(R.string.parallel_download), + notificationId = taskId.toNotificationId(), + progress = progress.toInt(), + text = text + ) + Downloader.updateTaskOutput( + url = url, line = text, progress = progress, isPlaylist = isPlaylist + ) + }) //clear all the lines that contains a "…" on it val finalResponse = removeDuplicateLines(clearLinesWithEllipsis(response.output)) - onTaskEnded(url, finalResponse) + onTaskEnded(url, finalResponse, name) }.onFailure { it.printStackTrace() ToastUtil.makeToastSuspend(context.getString(R.string.download_error_msg)) diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt index be7ded4e..3ff25465 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt @@ -218,6 +218,9 @@ object PreferencesUtil { fun isNetworkAvailableForDownload() = !App.connectivityManager.isActiveNetworkMetered //CELLULAR_DOWNLOAD.getBoolean() || + //check if the phone is connected to a network source (wifi or mobile data) + fun isNetworkAvailable() = App.connectivityManager.activeNetworkInfo?.isConnected == true + fun isAutoUpdateEnabled() = AUTO_UPDATE.getBoolean(!isFDroidBuild()) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 999cc8d8..7996ce12 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,7 +37,7 @@ Spotify URL or query Activate the usage of the app in background to make sure that all works fine. Battery configuration - General + General Appearance Change how the app looks like About @@ -308,4 +308,13 @@ Followers Here will appear all the downloads that you make from the searcher page. The log has been copied to the clipboard + Advanced features + Change more advanced settings of spotDL and parallel downloads + General settings + Use notifications + Pop up notifications with the actual downloads progress + Agree + Spotify API + This Spotify credentials can be used for having more closer and precise results to the songs requested in the main downloader page of the app. \n\nThis doesn\'t make effect on the searcher page because the app already uses extended-quota API credentials provided by Spotify. + Parallel download \ No newline at end of file From 18b5248198a446843868661c15eb3eeb6d6a7aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 1 Apr 2023 01:50:07 +0200 Subject: [PATCH 36/42] beaut: Added a horizontal pager in the search page for more comfort --- .../ui/pages/downloader/DownloaderPage.kt | 54 ++-- .../metadata_viewer/pages/PlaylistViewPage.kt | 2 +- .../spowlo/ui/pages/searcher/SearcherPage.kt | 268 ++++++++++++++---- .../pages/searcher/SearcherPageViewModel.kt | 6 + .../settings/appearance/AppearancePage.kt | 12 +- app/src/main/res/values/strings.xml | 2 + 6 files changed, 250 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index d03e2a76..653a27de 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -39,6 +39,7 @@ import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator @@ -172,8 +173,7 @@ fun DownloaderPage( .fillMaxSize() .background(MaterialTheme.colorScheme.background), ) { - DownloaderPageImplementation( - downloaderState = downloaderState, + DownloaderPageImplementation(downloaderState = downloaderState, taskState = taskState, viewState = viewState, errorState = errorState, @@ -199,8 +199,7 @@ fun DownloaderPage( onUrlChanged = { url -> downloaderViewModel.updateUrl(url) }) {} with(viewState) { - DownloaderSettingsDialog( - useDialog = useDialog, + DownloaderSettingsDialog(useDialog = useDialog, dialogState = showDownloadSettingDialog, drawerState = drawerState, confirm = { checkPermissionOrDownload() }, @@ -381,26 +380,29 @@ fun FABs( ) }, modifier = Modifier.padding(vertical = 12.dp) ) - ExtendedFloatingActionButton(onClick = downloadCallback, text = { - Text(stringResource(R.string.download)) - }, icon = { - Icon( - Icons.Outlined.FileDownload, - contentDescription = stringResource(R.string.download) - ) - }, modifier = Modifier.padding(vertical = 12.dp) - ) - AnimatedVisibility(visible = isDownloading) { - ExtendedFloatingActionButton( - text = { Text(stringResource(R.string.cancel)) }, - onClick = cancelCallback, icon = { - Icon( - Icons.Outlined.Cancel, - contentDescription = stringResource(R.string.cancel_download) - ) - }, modifier = Modifier.padding(vertical = 12.dp) - ) + Row(verticalAlignment = Alignment.CenterVertically) { + AnimatedVisibility(visible = isDownloading) { + FloatingActionButton( + onClick = cancelCallback, + content = { + Icon( + Icons.Outlined.Cancel, + contentDescription = stringResource(R.string.cancel_download) + ) + }, + modifier = Modifier.padding(horizontal = 12.dp), + ) + } + ExtendedFloatingActionButton(onClick = downloadCallback, text = { + Text(stringResource(R.string.download)) + }, icon = { + Icon( + Icons.Outlined.FileDownload, + contentDescription = stringResource(R.string.download) + ) + }, modifier = Modifier.padding(vertical = 12.dp)) } + } } @@ -442,7 +444,11 @@ fun InputUrl( 8.dp ), unfocusedBorderColor = MaterialTheme.colorScheme.surfaceVariant, - errorContainerColor = MaterialTheme.colorScheme.errorContainer.harmonizeWith(other = MaterialTheme.colorScheme.surfaceColorAtElevation(8.dp)), + errorContainerColor = MaterialTheme.colorScheme.errorContainer.harmonizeWith( + other = MaterialTheme.colorScheme.surfaceColorAtElevation( + 8.dp + ) + ), ), ) AnimatedVisibility(visible = showDownloadProgress) { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index 37199662..97393c7c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -114,7 +114,7 @@ fun PlaylistViewPage( onClick = { trackDownloadCallback(data.externalUrls.spotify!!, data.name) }, - modifier = Modifier.size(48.dp), + modifier = Modifier.size(48.dp).padding(12.dp), ) { Icon( imageVector = Icons.Filled.Download, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 83a2a8a8..f7f1d24b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -1,14 +1,20 @@ package com.bobbyesp.spowlo.ui.pages.searcher +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons @@ -21,6 +27,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.surfaceColorAtElevation @@ -28,10 +35,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager @@ -50,10 +59,15 @@ import com.bobbyesp.spowlo.ui.common.Route import com.bobbyesp.spowlo.ui.components.AutoResizableText import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.songs.search_feat.SearchingSongComponent +import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.IndicatorBehindScrollableTabRow +import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.getString +import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.tabIndicatorOffset +import com.bobbyesp.spowlo.ui.pages.common.ErrorPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfDataToString import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType import com.bobbyesp.spowlo.ui.theme.harmonizeWithPrimary import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable fun SearcherPage( @@ -61,7 +75,7 @@ fun SearcherPage( navController: NavController ) { val viewState by searcherPageViewModel.viewStateFlow.collectAsStateWithLifecycle() - + val scope = rememberCoroutineScope() Box( modifier = Modifier .fillMaxSize() @@ -73,6 +87,11 @@ fun SearcherPage( searcherPageViewModel.updateSearchText(query) }, onItemClick = { type, id -> navController.navigate(Route.PLAYLIST_PAGE + "/" + type + "/" + id) }, + reloadPageCallback = { + scope.launch { + searcherPageViewModel.makeSearch() + } + } ) } LaunchedEffect(viewState.query) { @@ -82,11 +101,13 @@ fun SearcherPage( } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun SearcherPageImpl( viewState: SearcherPageViewModel.ViewState, onValueChange: (String) -> Unit, onItemClick: (String, String) -> Unit, + reloadPageCallback : () -> Unit = {} ) { Scaffold(modifier = Modifier.fillMaxSize()) { with(viewState) { @@ -102,88 +123,204 @@ fun SearcherPageImpl( onValueChange(query) } ) - LazyColumn( + Column( modifier = Modifier .fillMaxSize() .padding(it) ) { when (viewState.viewState) { is ViewSearchState.Idle -> { - item { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background).padding(horizontal = 16.dp), - contentAlignment = Alignment.Center + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 16.dp), + contentAlignment = Alignment.TopCenter + ) { + AutoResizableText( + text = stringResource(id = R.string.search), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + } + } + + is ViewSearchState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - AutoResizableText( - text = stringResource(id = R.string.search), - style = MaterialTheme.typography.bodyMedium, + CircularProgressIndicator( + modifier = Modifier + .size(72.dp) + .padding(6.dp), + strokeWidth = 4.dp + ) + Text( + text = stringResource(id = R.string.loading), + modifier = Modifier.align( + Alignment.CenterHorizontally + ), + style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold ) } } + } - is ViewSearchState.Loading -> { - item { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - ) { - Column( - modifier = Modifier.align(Alignment.Center), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - CircularProgressIndicator( - modifier = Modifier - .size(72.dp) - .padding(6.dp), - strokeWidth = 4.dp - ) - Text( - text = stringResource(id = R.string.loading), - modifier = Modifier.align( - Alignment.CenterHorizontally - ), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) - } + is ViewSearchState.Error -> { + ErrorPage( + onReload = { reloadPageCallback() }, + exception = viewState.viewState.error, + modifier = Modifier.fillMaxSize() + ) + } + + is ViewSearchState.Success -> { + val pagerState = rememberPagerState(initialPage = 0) + val pages = listOf(SearcherPages.TRACKS, SearcherPages.PLAYLISTS) + val scope = rememberCoroutineScope() + IndicatorBehindScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + modifier = Modifier + .animateContentSize() + .fillMaxWidth(), + indicator = { tabPositions -> + Box( + Modifier + .padding(vertical = 12.dp) + .tabIndicatorOffset(tabPositions[pagerState.currentPage]) + .fillMaxHeight() + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondaryContainer) + ) + }, + edgePadding = 16.dp, + tabAlignment = Alignment.CenterStart, + ) { + pages.forEachIndexed { index, page -> + Tab( + text = { Text(text = page) }, + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + ) } } - } - is ViewSearchState.Error -> { - item { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - ) { - Column( - modifier = Modifier.align(Alignment.Center), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(R.string.searching_error), - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold - ) + HorizontalPager(pageCount = pages.size, state = pagerState, modifier = Modifier + .animateContentSize() + .fillMaxSize()) { + when (pages[it]) { + SearcherPages.TRACKS -> { + LazyColumn(modifier = Modifier.fillMaxSize()) { + viewState.viewState.data.let { data -> + item { + Text( + text = stringResource(R.string.showing_results).format( + data.tracks?.size + ), + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier + .padding(16.dp) + .alpha(0.7f), + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Start, + fontWeight = FontWeight.Bold + ) + + } + data.tracks?.items?.forEachIndexed { index, track -> + item { + val artists: List = + track.artists.map { artist -> artist.name } + SearchingSongComponent( + artworkUrl = track.album.images[2].url, + songName = track.name, + artists = artists.joinToString(", "), + spotifyUrl = track.externalUrls.spotify ?: "", + onClick = { onItemClick(track.type, track.id) }, + type = typeOfDataToString( + type = typeOfSpotifyDataType( + track.type + ) + ) + ) + HorizontalDivider( + modifier = Modifier.alpha(0.35f), + color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) + } + } + } + } + } + SearcherPages.PLAYLISTS -> { + LazyColumn(modifier = Modifier.fillMaxSize()) { + viewState.viewState.data.let { data -> + item { + Text( + text = stringResource(R.string.showing_results).format( + data.playlists?.size + ), + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier + .padding(16.dp) + .alpha(0.7f), + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Start, + fontWeight = FontWeight.Bold + ) + + } + data.playlists?.items?.forEachIndexed { index, playlist -> + item { + SearchingSongComponent( + artworkUrl = playlist.images[0].url, + songName = playlist.name, + artists = playlist.owner.displayName + ?: stringResource(R.string.unknown), + spotifyUrl = playlist.externalUrls.spotify ?: "", + onClick = { + onItemClick( + playlist.type, + playlist.id + ) + }, + type = typeOfDataToString( + type = typeOfSpotifyDataType( + playlist.type + ) + ) + ) + HorizontalDivider( + modifier = Modifier.alpha(0.35f), + color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) + } + } + } + } } } } - } - - is ViewSearchState.Success -> { + /* val allItems = mutableListOf() //TODO: Add the filters. Pagination should be done in the future viewState.viewState.data.let { data -> @@ -294,7 +431,7 @@ fun SearcherPageImpl( } } } - } + }*/ } } @@ -354,6 +491,11 @@ fun QueryTextBox( ) } +object SearcherPages { + val TRACKS = getString(R.string.tracks) + val PLAYLISTS = getString(R.string.playlists) +} + enum class FilterType { ALL, ALBUMS, ARTISTS, PLAYLISTS, TRACKS, EPISODES, SHOWS } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt index 0b61cc60..e422c0cf 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt @@ -33,6 +33,12 @@ class SearcherPageViewModel @Inject constructor() : ViewModel() { kotlin.runCatching { api.searchAllTypes(viewStateFlow.value.query) }.onSuccess { result -> + if (result == SpotifySearchResult(null, null, null, null, null, null)) { + mutableViewStateFlow.update { viewState -> + viewState.copy(viewState = ViewSearchState.Error("No results found")) + } + return@onSuccess + } mutableViewStateFlow.update { viewState -> viewState.copy(viewState = ViewSearchState.Success(result)) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt index c18775e0..2fd0a0b7 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt @@ -112,9 +112,9 @@ val colorList = listOf( fun AppearancePage( navController: NavHostController ) { - val scrollBehavior = - TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState(), - canScroll = { true }) + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState(), + canScroll = { true }) var showDarkThemeDialog by remember { mutableStateOf(false) } val darkTheme = LocalDarkTheme.current var darkThemeValue by remember { mutableStateOf(darkTheme.darkThemeValue) } @@ -133,7 +133,8 @@ fun AppearancePage( LargeTopAppBar(title = { Text( modifier = Modifier, - text = stringResource(id = R.string.display), fontWeight = FontWeight.Bold + text = stringResource(id = R.string.display), + fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { @@ -155,7 +156,7 @@ fun AppearancePage( explicit = true, cover_url = "https://i.scdn.co/image/ab67616d0000b273a152de6438e748b4c0cddff7", duration = 132.954 - ), modifier = Modifier.padding(16.dp) + ), modifier = Modifier.padding(16.dp), isPreview = true ) val pagerState = rememberPagerState(initialPage = colorList.indexOf(Color(LocalSeedColor.current)) @@ -199,7 +200,6 @@ fun AppearancePage( description = LocalDarkTheme.current.getDarkThemeDesc(), onChecked = { PreferencesUtil.modifyDarkThemePreference(if (isDarkTheme) OFF else ON) }, onClick = { navController.navigate(Route.APP_THEME) }) - //todo: add the languages page if (Build.VERSION.SDK_INT >= 24) PreferenceItem( title = stringResource(R.string.language), icon = Icons.Outlined.Language, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7996ce12..20a8d858 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -317,4 +317,6 @@ Spotify API This Spotify credentials can be used for having more closer and precise results to the songs requested in the main downloader page of the app. \n\nThis doesn\'t make effect on the searcher page because the app already uses extended-quota API credentials provided by Spotify. Parallel download + Tracks + Playlists \ No newline at end of file From d86164d8104b944e8aaa19cda80e68306d46e7cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 1 Apr 2023 02:29:23 +0200 Subject: [PATCH 37/42] feat: Created a new settings component and started custom extra-path implementation --- app/src/main/java/com/bobbyesp/spowlo/App.kt | 5 ++ .../components/settings/SettingsComponents.kt | 80 ++++++++++++++++-- .../metadata_viewer/pages/PlaylistViewPage.kt | 8 +- .../directories/DownloadsDirectoriesPage.kt | 84 +++++++++++-------- .../settings/format/SettingsFormatsPage.kt | 7 -- .../settings/spotify/SpotifySettingsPage.kt | 6 +- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 8 +- .../bobbyesp/spowlo/utils/PreferencesUtil.kt | 10 +-- 8 files changed, 148 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/App.kt b/app/src/main/java/com/bobbyesp/spowlo/App.kt index 96daaaab..bbb3c551 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/App.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/App.kt @@ -20,6 +20,7 @@ import com.bobbyesp.ffmpeg.FFmpeg import com.bobbyesp.library.SpotDL import com.bobbyesp.spowlo.utils.AUDIO_DIRECTORY import com.bobbyesp.spowlo.utils.DownloaderUtil +import com.bobbyesp.spowlo.utils.EXTRA_DIRECTORY import com.bobbyesp.spowlo.utils.FilesUtil import com.bobbyesp.spowlo.utils.FilesUtil.createEmptyFile import com.bobbyesp.spowlo.utils.FilesUtil.getCookiesFile @@ -74,6 +75,9 @@ class App : Application() { getString(R.string.app_name) ).absolutePath ) + extraDownloadDir = EXTRA_DIRECTORY.getString( + "" + ) if (Build.VERSION.SDK_INT >= 26) NotificationsUtil.createNotificationChannel() } @@ -81,6 +85,7 @@ class App : Application() { private const val PRIVATE_DIRECTORY_SUFFIX = ".Spowlo" lateinit var clipboard: ClipboardManager lateinit var audioDownloadDir: String + lateinit var extraDownloadDir: String lateinit var applicationScope: CoroutineScope lateinit var connectivityManager: ConnectivityManager lateinit var packageInfo: PackageInfo diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt index dd7684b7..ee8cc76c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt @@ -1,9 +1,15 @@ package com.bobbyesp.spowlo.ui.components.settings import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults @@ -11,6 +17,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip @@ -26,7 +33,7 @@ fun SettingsItemNew( icon: ImageVector? = null, addTonalElevation: Boolean = true, clipCorners: Boolean = false, - highlightIcon : Boolean = false + highlightIcon: Boolean = false ) { ListItem( modifier = Modifier @@ -45,7 +52,7 @@ fun SettingsItemNew( headlineContent = title, tonalElevation = if (addTonalElevation) 3.dp else 0.dp, colors = ListItemDefaults.colors( - leadingIconColor = if(highlightIcon) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, + leadingIconColor = if (highlightIcon) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, ) ) } @@ -61,7 +68,7 @@ fun SettingsItemNew( icon: ImageVector? = null, addTonalElevation: Boolean = true, clipCorners: Boolean = false, - highlightIcon : Boolean = false + highlightIcon: Boolean = false ) { SettingsItemNew( modifier = modifier @@ -122,12 +129,75 @@ fun SettingsSwitch( ) } +//settings switch with divider between the switch and the rest of the item. On click actions are independent of the switch +@Composable +fun SettingsSwitchWithDivider( + onCheckedChange: ((Boolean) -> Unit)?, + checked: Boolean, + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + description: (@Composable () -> Unit)? = null, + icon: ImageVector? = null, + thumbContent: (@Composable () -> Unit)? = null, + addTonalElevation: Boolean = true, + clipCorners: Boolean = false, + highlightIcon: Boolean = false, + onClick: () -> Unit = {} +) { + val toggleableModifier = if (onCheckedChange != null) { + Modifier.toggleable( + value = checked, + enabled = enabled, + onValueChange = onCheckedChange + ).apply { if (!enabled) this.alpha(0.5f) } + } else Modifier + + SettingsItemNew( + modifier = modifier + .then(toggleableModifier), + icon = icon, + description = description, + title = title, + onClick = { onClick() }, + trailing = { + Row( + modifier = Modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Divider( + modifier = Modifier + .height(32.dp) + .padding(horizontal = 8.dp) + .width(1f.dp) + .align(Alignment.CenterVertically), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + ) + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + thumbContent = thumbContent + ) + } + }, + addTonalElevation = addTonalElevation, + clipCorners = clipCorners, + highlightIcon = highlightIcon + ) +} + @Composable fun ElevatedSettingsCard( - content : @Composable () -> Unit + content: @Composable () -> Unit ) { Card( - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( + 3.dp + ) + ) ) { content() } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index 97393c7c..50e71d09 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -109,12 +109,16 @@ fun PlaylistViewPage( modifier = Modifier.alpha(alpha = 0.8f) ) } - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically) { + Row( + modifier = Modifier.fillMaxWidth().padding(12.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { FilledTonalIconButton( onClick = { trackDownloadCallback(data.externalUrls.spotify!!, data.name) }, - modifier = Modifier.size(48.dp).padding(12.dp), + modifier = Modifier.size(48.dp), ) { Icon( imageVector = Icons.Filled.Download, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/directories/DownloadsDirectoriesPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/directories/DownloadsDirectoriesPage.kt index 0bd75add..811c98ed 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/directories/DownloadsDirectoriesPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/directories/DownloadsDirectoriesPage.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.SdCardAlert import androidx.compose.material.icons.outlined.LibraryMusic @@ -33,6 +34,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext @@ -46,17 +48,15 @@ import com.bobbyesp.spowlo.ui.common.LocalDarkTheme import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.LargeTopAppBar import com.bobbyesp.spowlo.ui.components.PreferenceInfo -import com.bobbyesp.spowlo.ui.components.PreferenceItem -import com.bobbyesp.spowlo.ui.components.PreferenceSwitchWithDivider import com.bobbyesp.spowlo.ui.components.PreferencesHintCard -import com.bobbyesp.spowlo.utils.CUSTOM_COMMAND +import com.bobbyesp.spowlo.ui.components.settings.SettingsItemNew +import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitchWithDivider import com.bobbyesp.spowlo.utils.CUSTOM_PATH import com.bobbyesp.spowlo.utils.FilesUtil import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.PreferencesUtil.getString import com.bobbyesp.spowlo.utils.SDCARD_DOWNLOAD import com.bobbyesp.spowlo.utils.SDCARD_URI -import com.bobbyesp.spowlo.utils.SUBDIRECTORY import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState @@ -65,6 +65,7 @@ private const val validDirectoryRegex = "/storage/emulated/0/(Download|Documents private fun String.isValidDirectory(): Boolean { return this.contains(Regex(validDirectoryRegex)) } + private enum class Directory { AUDIO, SDCARD } @@ -85,8 +86,6 @@ fun DownloadsDirectoriesPage( val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } - var isSubdirectoryEnabled - by remember { mutableStateOf(PreferencesUtil.getValue(SUBDIRECTORY)) } var isCustomPathEnabled by remember { mutableStateOf(PreferencesUtil.getValue(CUSTOM_PATH)) } @@ -101,16 +100,12 @@ fun DownloadsDirectoriesPage( mutableStateOf(PreferencesUtil.getValue(SDCARD_DOWNLOAD)) } - var pathTemplateText by remember { mutableStateOf(PreferencesUtil.getOutputPathTemplate()) } + var pathTemplateText by remember { mutableStateOf(PreferencesUtil.getExtraDirectory()) } var showClearTempDialog by remember { mutableStateOf(false) } var editingDirectory by remember { mutableStateOf(Directory.AUDIO) } - val isCustomCommandEnabled by remember { - mutableStateOf(PreferencesUtil.getValue(CUSTOM_COMMAND)) - } - val storagePermission = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) @@ -141,7 +136,7 @@ fun DownloadsDirectoriesPage( } val path = FilesUtil.getRealPath(it) App.updateDownloadDir(path) - audioDirectoryText = path + audioDirectoryText = path } } @@ -166,7 +161,8 @@ fun DownloadsDirectoriesPage( title = { Text( modifier = Modifier, - text = stringResource(id = R.string.download_directory), fontWeight = FontWeight.Bold + text = stringResource(id = R.string.download_directory), + fontWeight = FontWeight.Bold ) }, navigationIcon = { BackButton { @@ -175,8 +171,12 @@ fun DownloadsDirectoriesPage( }, scrollBehavior = scrollBehavior ) }, content = { - LazyColumn(modifier = Modifier.padding(it)) { - if(sdcardUri.isEmpty()) + LazyColumn( + modifier = Modifier + .padding(it) + .padding(horizontal = 16.dp) + ) { + if (sdcardUri.isEmpty()) item { PreferenceInfo(text = stringResource(id = R.string.sdcard_not_activable_hint)) } @@ -203,38 +203,54 @@ fun DownloadsDirectoriesPage( text = stringResource(id = R.string.general), modifier = Modifier .fillMaxWidth() - .padding(start = 18.dp, top = 12.dp, bottom = 4.dp), + .padding(start = 2.dp, top = 12.dp, bottom = 4.dp), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelLarge ) } - item{ - PreferenceItem( - title = stringResource(id = R.string.audio_directory), - description = audioDirectoryText, - enabled = !isCustomCommandEnabled && !sdcardDownload, - icon = Icons.Outlined.LibraryMusic - ) { - editingDirectory = Directory.AUDIO - openDirectoryChooser() - } + item { + SettingsItemNew( + onClick = { + editingDirectory = Directory.AUDIO + openDirectoryChooser() + }, + title = { + Text( + text = stringResource(id = R.string.audio_directory), + fontWeight = FontWeight.Bold + ) + }, + description = { Text(text = audioDirectoryText) }, + icon = Icons.Outlined.LibraryMusic, + modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)) + ) } item { - PreferenceSwitchWithDivider( - title = stringResource(id = R.string.sdcard_directory), - description = sdcardUri, - isChecked = sdcardDownload, - enabled = !isCustomCommandEnabled, - isSwitchEnabled = !isCustomCommandEnabled && sdcardUri.isNotBlank(), - onChecked = { + SettingsSwitchWithDivider( + onCheckedChange = { sdcardDownload = !sdcardDownload PreferencesUtil.updateValue(SDCARD_DOWNLOAD, sdcardDownload) }, + checked = sdcardDownload, + title = { + Text( + text = stringResource(id = R.string.sdcard_directory), + fontWeight = FontWeight.Bold + ) + }, + description = { + Text( + text = sdcardUri, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + }, + enabled = sdcardUri.isNotBlank(), icon = Icons.Outlined.SdCard, onClick = { editingDirectory = Directory.SDCARD openDirectoryChooser() - } + }, + modifier = Modifier.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)) ) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt index 2d3be8ba..d34ae424 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt @@ -28,7 +28,6 @@ import com.bobbyesp.spowlo.ui.components.LargeTopAppBar import com.bobbyesp.spowlo.ui.components.PreferenceItem import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle import com.bobbyesp.spowlo.ui.components.PreferenceSwitch -import com.bobbyesp.spowlo.utils.CUSTOM_COMMAND import com.bobbyesp.spowlo.utils.ORIGINAL_AUDIO import com.bobbyesp.spowlo.utils.PreferencesUtil @@ -67,11 +66,6 @@ fun SettingsFormatsPage(onBackPressed: () -> Unit) { }, scrollBehavior = scrollBehavior ) }, content = { - val isCustomCommandEnabled by remember { - mutableStateOf( - PreferencesUtil.getValue(CUSTOM_COMMAND) - ) - } LazyColumn(Modifier.padding(it)) { item { PreferenceSubtitle(text = stringResource(id = R.string.audio)) @@ -109,7 +103,6 @@ fun SettingsFormatsPage(onBackPressed: () -> Unit) { title = stringResource(R.string.audio_provider), description = stringResource(R.string.audio_provider_desc), icon = Icons.Outlined.ShuffleOn, - enabled = !isCustomCommandEnabled, ) { showAudioProviderDialog = true } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt index c67a9eef..2ccf6ecb 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/spotify/SpotifySettingsPage.kt @@ -119,7 +119,7 @@ fun SpotifySettingsPage(onBackPressed: () -> Unit) { ) { SettingsSwitch( title = { - Text(stringResource(id = R.string.use_spotify_credentials)) + Text(stringResource(id = R.string.use_spotify_credentials), fontWeight = FontWeight.Bold) }, checked = useSpotifyCredentials, onCheckedChange = { @@ -134,7 +134,7 @@ fun SpotifySettingsPage(onBackPressed: () -> Unit) { Divider(color = MaterialTheme.colorScheme.surfaceVariant) SettingsItemNew( title = { - Text(stringResource(id = R.string.spotify_client_id)) + Text(stringResource(id = R.string.spotify_client_id), fontWeight = FontWeight.Bold) }, description = { Text(stringResource(id = R.string.spotify_client_id_description)) @@ -149,7 +149,7 @@ fun SpotifySettingsPage(onBackPressed: () -> Unit) { SettingsItemNew( title = { - Text(stringResource(id = R.string.spotify_client_secret)) + Text(stringResource(id = R.string.spotify_client_secret), fontWeight = FontWeight.Bold) }, description = { Text(stringResource(id = R.string.spotify_client_secret_description)) diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index d980d3b5..aca672f0 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -49,9 +49,7 @@ object DownloaderUtil { data class DownloadPreferences( val downloadPlaylist: Boolean = PreferencesUtil.getValue(PLAYLIST), - val subdirectory: Boolean = PreferencesUtil.getValue(SUBDIRECTORY), val customPath: Boolean = PreferencesUtil.getValue(CUSTOM_PATH), - val outputPathTemplate: String = PreferencesUtil.getOutputPathTemplate(), val maxFileSize: String = MAX_FILE_SIZE.getString(), val cookies: Boolean = PreferencesUtil.getValue(COOKIES), val cookiesContent: String = PreferencesUtil.getCookies(), @@ -71,6 +69,7 @@ object DownloaderUtil { val privateMode: Boolean = PreferencesUtil.getValue(PRIVATE_MODE), val sdcard: Boolean = PreferencesUtil.getValue(SDCARD_DOWNLOAD), val sdcardUri: String = SDCARD_URI.getString(), + val extraDirectory: String = PreferencesUtil.getExtraDirectory(), val threads: Int = THREADS.getInt() ) @@ -233,6 +232,11 @@ object DownloaderUtil { addOption("download", url) pathBuilder.append(audioDownloadDir) + + if (extraDirectory.isNotEmpty()) { + pathBuilder.append("/").append(extraDirectory) + } + Log.d(TAG, "downloadSong: $pathBuilder") addOption("--output", pathBuilder.toString()) diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt index 3ff25465..35063b63 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/PreferencesUtil.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -const val CUSTOM_COMMAND = "custom_command" const val SPOTDL = "spotDL_Init" const val DEBUG = "debug" const val CONFIGURE = "configure" @@ -37,17 +36,16 @@ const val AUDIO_FORMAT = "audio_format" const val AUDIO_QUALITY = "audio_quality" const val WELCOME_DIALOG = "welcome_dialog" const val AUDIO_DIRECTORY = "audio_dir" +const val EXTRA_DIRECTORY = "extra_dir" const val ORIGINAL_AUDIO = "original_audio" const val SDCARD_DOWNLOAD = "sdcard_download" const val SDCARD_URI = "sd_card_uri" -const val SUBDIRECTORY = "sub-directory" const val PLAYLIST = "playlist" const val LANGUAGE = "language" const val NOTIFICATION = "notification" private const val THEME_COLOR = "theme_color" const val PALETTE_STYLE = "palette_style" const val CUSTOM_PATH = "custom_path" -const val OUTPUT_PATH_TEMPLATE = "path_template" const val USE_YT_METADATA = "use_yt_metadata" const val USE_SPOTIFY_CREDENTIALS = "use_spotify_credentials" @@ -82,11 +80,9 @@ const val SYSTEM_DEFAULT = 0 const val STABLE = 0 const val PRE_RELEASE = 1 -const val TEMPLATE_EXAMPLE = """--audio youtube-music --dont-filter-results""" - private val StringPreferenceDefaults = mapOf( - OUTPUT_PATH_TEMPLATE to "{artists} - {title}.{output-ext}", + EXTRA_DIRECTORY to "", SPOTIFY_CLIENT_ID to "", SPOTIFY_CLIENT_SECRET to "", ) @@ -152,7 +148,7 @@ object PreferencesUtil { fun encodeString(key: String, string: String) = key.updateString(string) fun containsKey(key: String) = kv.containsKey(key) - fun getOutputPathTemplate(): String = OUTPUT_PATH_TEMPLATE.getString() + fun getExtraDirectory(): String = EXTRA_DIRECTORY.getString() fun getAudioFormat(): Int = AUDIO_FORMAT.getInt() From 885494f0e56b0c61d4d3571ade6fb13869703e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Sat, 1 Apr 2023 16:39:45 +0200 Subject: [PATCH 38/42] bugfix: Fixed some crashes due to searcher page and almost finished moving to the new settings components --- .../java/com/bobbyesp/spowlo/Downloader.kt | 2 +- .../data/remote/SpotifyApiRequests.kt | 26 +- .../components/settings/SettingsComponents.kt | 41 +++ .../metadata_viewer/pages/PlaylistViewPage.kt | 48 ++-- .../pages/metadata_viewer/pages/TrackPage.kt | 42 ++- .../playlists/PlaylistPageViewModel.kt | 6 + .../spowlo/ui/pages/searcher/SearcherPage.kt | 244 +----------------- .../pages/searcher/SearcherPageViewModel.kt | 2 - .../appearance/AppThemePreferencesPage.kt | 49 +++- .../settings/appearance/AppearancePage.kt | 72 ++++-- .../pages/settings/appearance/LanguagePage.kt | 42 ++- .../settings/format/SettingsFormatsPage.kt | 122 +++++---- .../settings/general/GeneralSettingsPage.kt | 88 ++++--- app/src/main/res/values-es/strings.xml | 4 +- app/src/main/res/values/strings.xml | 4 +- 15 files changed, 356 insertions(+), 436 deletions(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index 21e1d0ad..8dac163e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -96,7 +96,7 @@ object Downloader { fun onRestart() { applicationScope.launch(Dispatchers.IO) { - getInfoAndDownload(url, skipInfoFetch = true) + executeParallelDownloadWithUrl(url, name = taskName) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt index 1fb2b4d1..ecdd346b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/features/spotify_api/data/remote/SpotifyApiRequests.kt @@ -5,7 +5,6 @@ import com.adamratzman.spotify.SpotifyAppApi import com.adamratzman.spotify.models.Album import com.adamratzman.spotify.models.Artist import com.adamratzman.spotify.models.AudioFeatures -import com.adamratzman.spotify.models.PagingObject import com.adamratzman.spotify.models.Playlist import com.adamratzman.spotify.models.SpotifyPublicUser import com.adamratzman.spotify.models.SpotifySearchResult @@ -87,7 +86,7 @@ object SpotifyApiRequests { private suspend fun searchTracks(searchQuery: String): List { kotlin.runCatching { - provideSpotifyApi().search.searchTrack(searchQuery, limit = 50, offset = 1, market = Market.ES) + provideSpotifyApi().search.searchTrack(searchQuery, limit = 50) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") return listOf() @@ -103,27 +102,10 @@ object SpotifyApiRequests { return searchTracks(query) } - suspend fun searchTracksForPaging(searchQuery: String, nextPageNumber: Int): PagingObject? { - kotlin.runCatching { - provideSpotifyApi().search.searchTrack(searchQuery, limit = 50, offset = nextPageNumber, market = Market.ES) - }.onFailure { - Log.d("SpotifyApiRequests", "Error: ${it.message}") - }.onSuccess { - return it - } - return null - } - - @Provides - @Singleton - suspend fun provideSearchTracksForPaging(query: String, nextPageNumber: Int): PagingObject? { - return searchTracksForPaging(query, nextPageNumber) - } - //search by id suspend fun getPlaylistById(id: String): Playlist? { kotlin.runCatching { - provideSpotifyApi().playlists.getPlaylist(id, market = Market.ES) + provideSpotifyApi().playlists.getPlaylist(id) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") return null @@ -142,7 +124,7 @@ object SpotifyApiRequests { suspend fun getTrackById(id: String): Track? { kotlin.runCatching { - provideSpotifyApi().tracks.getTrack(id, market = Market.ES) + provideSpotifyApi().tracks.getTrack(id) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") return null @@ -178,7 +160,7 @@ object SpotifyApiRequests { suspend fun getAlbumById(id: String): Album? { kotlin.runCatching { - provideSpotifyApi().albums.getAlbum(id, market = Market.ES) + provideSpotifyApi().albums.getAlbum(id) }.onFailure { Log.d("SpotifyApiRequests", "Error: ${it.message}") return null diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt index ee8cc76c..9a844971 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/settings/SettingsComponents.kt @@ -2,7 +2,9 @@ package com.bobbyesp.spowlo.ui.components.settings import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -14,7 +16,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton import androidx.compose.material3.Switch +import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -22,6 +26,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @Composable @@ -129,6 +135,41 @@ fun SettingsSwitch( ) } +@Composable +fun SettingsNewSingleChoiceItem( + modifier: Modifier = Modifier, + text: String, + selected: Boolean, + wantsTonalElevation: Boolean = true, + contentPadding: PaddingValues = PaddingValues(0.dp), + onClick: () -> Unit, +) { + ListItem( + headlineContent = { + Text( + text = text, + maxLines = 1, + fontWeight = FontWeight.Bold + ) + }, + trailingContent = { + RadioButton( + selected = selected, + onClick = onClick + ) + }, + modifier = Modifier + .fillMaxWidth() + .clearAndSetSemantics { } + .clickable( + onClick = onClick, + ) + .padding(contentPadding) + .then(modifier), + tonalElevation = if (wantsTonalElevation) 3.dp else 0.dp, + ) +} + //settings switch with divider between the switch and the rest of the item. On click actions are independent of the switch @Composable fun SettingsSwitchWithDivider( diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index 50e71d09..4be7dfd2 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -109,26 +109,28 @@ fun PlaylistViewPage( modifier = Modifier.alpha(alpha = 0.8f) ) } - Row( - modifier = Modifier.fillMaxWidth().padding(12.dp), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically - ) { - FilledTonalIconButton( - onClick = { - trackDownloadCallback(data.externalUrls.spotify!!, data.name) - }, - modifier = Modifier.size(48.dp), + if(data.externalUrls.spotify != null){ + Row( + modifier = Modifier.fillMaxWidth().padding(12.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically ) { - Icon( - imageVector = Icons.Filled.Download, - contentDescription = "Download full playlist icon", - modifier = Modifier - .weight(1f) - .padding(14.dp) - ) - } + FilledTonalIconButton( + onClick = { + trackDownloadCallback(data.externalUrls.spotify!!, data.name) + }, + modifier = Modifier.size(48.dp), + ) { + Icon( + imageVector = Icons.Filled.Download, + contentDescription = "Download full playlist icon", + modifier = Modifier + .weight(1f) + .padding(14.dp) + ) + } + } } HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) Column( @@ -142,9 +144,13 @@ fun PlaylistViewPage( contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), songName = actualTrack?.name ?: App.context.getString(R.string.unknown), artists = actualTrack?.artists?.joinToString(", ") { it.name } ?: "", - spotifyUrl = actualTrack?.externalUrls?.spotify!!, - isExplicit = actualTrack.explicit, - onClick = { trackDownloadCallback(actualTrack.externalUrls.spotify!!, taskName) } + spotifyUrl = actualTrack?.externalUrls?.spotify ?: "", + isExplicit = actualTrack?.explicit ?: false, + onClick = { + if (actualTrack != null) { + trackDownloadCallback(actualTrack.externalUrls.spotify!!, taskName) + } + } ) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index 6729ad59..dca76eb7 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -1,5 +1,6 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages +import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,7 +20,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -40,7 +44,12 @@ import com.bobbyesp.spowlo.ui.components.songs.metadata_viewer.ExtraInfoCard import com.bobbyesp.spowlo.ui.components.songs.metadata_viewer.TrackComponent import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.dataStringToString import com.bobbyesp.spowlo.utils.GeneralTextUtils +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +@OptIn(ExperimentalSerializationApi::class) @Composable fun TrackPage( data: Track, @@ -48,14 +57,17 @@ fun TrackPage( trackDownloadCallback: (String, String) -> Unit, ) { val localConfig = LocalConfiguration.current - val audioFeatures by remember { - mutableStateOf(mutableStateOf(null)) + var audioFeatures by rememberSaveable(stateSaver = AudioFeaturesSaver) { + mutableStateOf(null) } - LaunchedEffect(Unit){ - val feats = SpotifyApiRequests.providesGetAudioFeatures(data.id) - audioFeatures.value = feats + LaunchedEffect(Unit) { + if (audioFeatures == null) { + val feats = SpotifyApiRequests.providesGetAudioFeatures(data.id) + audioFeatures = feats + } } + Column( modifier = modifier.fillMaxSize() ) { @@ -145,7 +157,7 @@ fun TrackPage( modifier = Modifier.weight(1f) ) } - AnimatedVisibility(visible = audioFeatures.value != null) { + AnimatedVisibility(visible = audioFeatures != null) { Row( modifier = Modifier .fillMaxWidth() @@ -153,17 +165,29 @@ fun TrackPage( ) { ExtraInfoCard( headlineText = stringResource(id = R.string.loudness), - bodyText = audioFeatures.value!!.loudness.toString(), + bodyText = audioFeatures!!.loudness.toString(), modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(16.dp)) ExtraInfoCard( headlineText = stringResource(id = R.string.tempo), - bodyText = audioFeatures.value!!.tempo.toString() + " BPM", + bodyText = audioFeatures!!.tempo.toString() + " BPM", modifier = Modifier.weight(1f) ) } } } } +} + +//create a saver for the audio features +@ExperimentalSerializationApi +object AudioFeaturesSaver : Saver { + override fun restore(value: String): AudioFeatures? { + return Json.decodeFromString(value) + } + + override fun SaverScope.save(value: AudioFeatures?): String { + return Json.encodeToString(value) + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt index ca899fbd..13153b5e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt @@ -20,6 +20,12 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { ) suspend fun loadData(id: String, type: SpotifyDataType = SpotifyDataType.TRACK) { + mutableViewStateFlow.update { + it.copy( + id = id, + state = PlaylistDataState.Loading + ) + } when (type) { SpotifyDataType.TRACK -> { kotlin.runCatching { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index f7f1d24b..d6fcb2ce 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -320,119 +320,6 @@ fun SearcherPageImpl( } } } - /* - val allItems = - mutableListOf() //TODO: Add the filters. Pagination should be done in the future - viewState.viewState.data.let { data -> - data.albums?.items?.let {/* allItems.addAll(it)*/ } - data.artists?.items?.let { /*allItems.addAll(it) */ } - data.playlists?.items?.let { allItems.addAll(it) } - data.tracks?.items?.let { allItems.addAll(it) } - data.episodes?.items?.let {/* - allItems.addAll( - listOf( - it - ) - )*/ - } - data.shows?.items?.let {/* - allItems.addAll( - listOf( - it - ) - )*/ - } - if (data != null) { //You may think that this is not necessary, but it is - item { - Text( - text = stringResource(R.string.showing_results).format( - allItems.size - ), - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Bold - ), - modifier = Modifier - .padding(16.dp) - .alpha(0.7f), - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Start, - fontWeight = FontWeight.Bold - ) - - } - data.albums?.items?.forEachIndexed { index, album -> - item { - // TODO: Display the album item - } - } - data.artists?.items?.forEachIndexed { index, artist -> - item { - // TODO: Display the artist item - } - } - - data.tracks?.items?.forEachIndexed { index, track -> - item { - val artists: List = - track.artists.map { artist -> artist.name } - SearchingSongComponent( - artworkUrl = track.album.images[2].url, - songName = track.name, - artists = artists.joinToString(", "), - spotifyUrl = track.externalUrls.spotify ?: "", - onClick = { onItemClick(track.type, track.id) }, - type = typeOfDataToString( - type = typeOfSpotifyDataType( - track.type - ) - ) - ) - HorizontalDivider( - modifier = Modifier.alpha(0.35f), - color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() - ) - } - } - - data.playlists?.items?.forEachIndexed { index, playlist -> - item { - SearchingSongComponent( - artworkUrl = playlist.images[0].url, - songName = playlist.name, - artists = playlist.owner.displayName - ?: stringResource(R.string.unknown), - spotifyUrl = playlist.externalUrls.spotify ?: "", - onClick = { - onItemClick( - playlist.type, - playlist.id - ) - }, - type = typeOfDataToString( - type = typeOfSpotifyDataType( - playlist.type - ) - ) - ) - HorizontalDivider( - modifier = Modifier.alpha(0.35f), - color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() - ) - } - } - data.episodes?.items?.forEachIndexed { index, episode -> - item { - // TODO: Display the episode item - } - } - data.shows?.items?.forEachIndexed { index, show -> - item { - // TODO: Display the show item - } - } - } - }*/ - } } } @@ -494,133 +381,4 @@ fun QueryTextBox( object SearcherPages { val TRACKS = getString(R.string.tracks) val PLAYLISTS = getString(R.string.playlists) -} - -enum class FilterType { - ALL, ALBUMS, ARTISTS, PLAYLISTS, TRACKS, EPISODES, SHOWS -} -//TODO: Add filters -/* -* val filterState = rememberSaveable { mutableStateOf(FilterType.ALL) } - - // Filter the items based on the selected filter type - val filteredItems = when (filterState.value) { - FilterType.ALL -> allItems - FilterType.ALBUMS -> allItems.filterIsInstance() - FilterType.ARTISTS -> allItems.filterIsInstance() - FilterType.PLAYLISTS -> allItems.filterIsInstance() - FilterType.TRACKS -> allItems.filterIsInstance() - FilterType.EPISODES -> allItems.filterIsInstance() - FilterType.SHOWS -> allItems.filterIsInstance() - } -* */ -// -------------------------------------------- - -/* -* val allItems = - mutableListOf() //TODO: Add the filters. Pagination should be done in the future - viewState.viewState.data.let { data -> - data.albums?.items?.let { allItems.addAll(it) } - data.artists?.items?.let { allItems.addAll(it) } - data.playlists?.items?.let { allItems.addAll(it) } - data.tracks?.items?.let { allItems.addAll(it) } - data.episodes?.items?.let { - allItems.addAll( - listOf( - it - ) - ) - } - data.shows?.items?.let { - allItems.addAll( - listOf( - it - ) - ) - } - if (data != null) { //You may think that this is not necessary, but it is - item { - Text( - text = stringResource(R.string.showing_results).format( - allItems.size - ), - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Bold - ), - modifier = Modifier - .padding(16.dp) - .alpha(0.7f), - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Start, - fontWeight = FontWeight.Bold - ) - - } - data.albums?.items?.forEachIndexed { index, album -> - item { - // TODO: Display the album item - } - } - data.artists?.items?.forEachIndexed { index, artist -> - item { - // TODO: Display the artist item - } - } - - data.tracks?.items?.forEachIndexed { index, track -> - item { - val artists: List = - track.artists.map { artist -> artist.name } - SearchingSongComponent( - artworkUrl = track.album.images[2].url, - songName = track.name, - artists = artists.joinToString(", "), - spotifyUrl = track.externalUrls.spotify ?: "", - onClick = { onItemClick(track.type ,track.id) }, - type = typeOfDataToString( - type = typeOfSpotifyDataType( - track.type - ) - ) - ) - HorizontalDivider( - modifier = Modifier.alpha(0.35f), - color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() - ) - } - } - - data.playlists?.items?.forEachIndexed { index, playlist -> - item { - SearchingSongComponent( - artworkUrl = playlist.images[0].url, - songName = playlist.name, - artists = playlist.owner.displayName - ?: stringResource(R.string.unknown), - spotifyUrl = playlist.externalUrls.spotify ?: "", - onClick = { onItemClick(playlist.type ,playlist.id) }, - type = typeOfDataToString( - type = typeOfSpotifyDataType( - playlist.type - ) - ) - ) - HorizontalDivider( - modifier = Modifier.alpha(0.35f), - color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() - ) - } - } - data.episodes?.items?.forEachIndexed { index, episode -> - item { - // TODO: Display the episode item - } - } - data.shows?.items?.forEachIndexed { index, show -> - item { - // TODO: Display the show item - } - } - } - } - * */ \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt index e422c0cf..bf2c907e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPageViewModel.kt @@ -1,6 +1,5 @@ package com.bobbyesp.spowlo.ui.pages.searcher -import android.util.Log import androidx.lifecycle.ViewModel import com.adamratzman.spotify.models.SpotifySearchResult import com.bobbyesp.spowlo.features.spotify_api.data.remote.SpotifyApiRequests @@ -42,7 +41,6 @@ class SearcherPageViewModel @Inject constructor() : ViewModel() { mutableViewStateFlow.update { viewState -> viewState.copy(viewState = ViewSearchState.Success(result)) } - Log.d("SearcherPageViewModel", "makeSearch: $result") }.onFailure { mutableViewStateFlow.update { viewState -> viewState.copy(viewState = ViewSearchState.Error(it.message.toString())) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppThemePreferencesPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppThemePreferencesPage.kt index 481dfbe1..cab16e6b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppThemePreferencesPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppThemePreferencesPage.kt @@ -3,6 +3,7 @@ package com.bobbyesp.spowlo.ui.pages.settings.appearance import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Contrast import androidx.compose.material3.ExperimentalMaterial3Api @@ -13,6 +14,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -20,9 +22,9 @@ import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.LocalDarkTheme import com.bobbyesp.spowlo.ui.components.BackButton -import com.bobbyesp.spowlo.ui.components.PreferenceSingleChoiceItem import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle -import com.bobbyesp.spowlo.ui.components.PreferenceSwitch +import com.bobbyesp.spowlo.ui.components.settings.SettingsNewSingleChoiceItem +import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitch import com.bobbyesp.spowlo.utils.DarkThemePreference.Companion.FOLLOW_SYSTEM import com.bobbyesp.spowlo.utils.DarkThemePreference.Companion.OFF import com.bobbyesp.spowlo.utils.DarkThemePreference.Companion.ON @@ -55,36 +57,55 @@ fun AppThemePreferencesPage(onBackPressed: () -> Unit) { }, scrollBehavior = scrollBehavior ) }, content = { - LazyColumn(modifier = Modifier.padding(it)) { + LazyColumn( + modifier = Modifier + .padding(it) + .padding(16.dp) + ) { item { - PreferenceSingleChoiceItem( + SettingsNewSingleChoiceItem( text = stringResource(R.string.follow_system), - selected = darkThemePreference.darkThemeValue == FOLLOW_SYSTEM + selected = darkThemePreference.darkThemeValue == FOLLOW_SYSTEM, + modifier = Modifier.clip( + RoundedCornerShape( + topStart = 16.dp, + topEnd = 16.dp + ) + ) ) { PreferencesUtil.modifyDarkThemePreference(FOLLOW_SYSTEM) } } + item { - PreferenceSingleChoiceItem( + SettingsNewSingleChoiceItem( text = stringResource(R.string.on), selected = darkThemePreference.darkThemeValue == ON ) { PreferencesUtil.modifyDarkThemePreference(ON) } } + item { - PreferenceSingleChoiceItem( + SettingsNewSingleChoiceItem( text = stringResource(R.string.off), - selected = darkThemePreference.darkThemeValue == OFF + selected = darkThemePreference.darkThemeValue == OFF, + modifier = Modifier.clip( + RoundedCornerShape( + bottomStart = 16.dp, + bottomEnd = 16.dp + ) + ) ) { PreferencesUtil.modifyDarkThemePreference(OFF) } } item { PreferenceSubtitle(text = stringResource(R.string.additional_settings)) } item { - PreferenceSwitch( - title = stringResource(R.string.high_contrast), - icon = Icons.Outlined.Contrast, - isChecked = isHighContrastModeEnabled, onClick = { + SettingsSwitch( + onCheckedChange = { PreferencesUtil.modifyDarkThemePreference(isHighContrastModeEnabled = !isHighContrastModeEnabled) - } - ) + }, + checked = isHighContrastModeEnabled, + title = { Text(text = stringResource(R.string.high_contrast)) }, + icon = Icons.Outlined.Contrast, + clipCorners = true) } } }) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt index 2fd0a0b7..e4dcb10f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/AppearancePage.kt @@ -64,10 +64,10 @@ import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.ConfirmButton import com.bobbyesp.spowlo.ui.components.DismissButton import com.bobbyesp.spowlo.ui.components.LargeTopAppBar -import com.bobbyesp.spowlo.ui.components.PreferenceItem -import com.bobbyesp.spowlo.ui.components.PreferenceSwitch -import com.bobbyesp.spowlo.ui.components.PreferenceSwitchWithDivider import com.bobbyesp.spowlo.ui.components.SingleChoiceItem +import com.bobbyesp.spowlo.ui.components.settings.SettingsItemNew +import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitch +import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitchWithDivider import com.bobbyesp.spowlo.ui.components.songs.SongCard import com.bobbyesp.spowlo.ui.theme.DEFAULT_SEED_COLOR import com.bobbyesp.spowlo.utils.DarkThemePreference.Companion.FOLLOW_SYSTEM @@ -147,6 +147,7 @@ fun AppearancePage( Column( Modifier .padding(it) + .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()) ) { SongCard( @@ -167,7 +168,7 @@ fun AppearancePage( .clearAndSetSemantics { }, state = pagerState, pageCount = colorList.size, - contentPadding = PaddingValues(horizontal = 12.dp) + contentPadding = PaddingValues(horizontal = 6.dp) ) { Row { ColorButtons(colorList[it]) } } @@ -183,34 +184,57 @@ fun AppearancePage( indicatorWidth = 6.dp) if (DynamicColors.isDynamicColorAvailable()) { - PreferenceSwitch(title = stringResource(id = R.string.dynamic_color), - description = stringResource( - id = R.string.dynamic_color_desc - ), + SettingsSwitch( + title = { + Text( + stringResource(id = R.string.dynamic_color), + fontWeight = FontWeight.Bold + ) + }, + description = { + Text( + stringResource(id = R.string.dynamic_color_desc) + ) + }, icon = Icons.Outlined.Palette, - isChecked = LocalDynamicColorSwitch.current, - onClick = { + checked = LocalDynamicColorSwitch.current, + onCheckedChange = { PreferencesUtil.switchDynamicColor() - }) + }, modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))) } val isDarkTheme = LocalDarkTheme.current.isDarkTheme() - PreferenceSwitchWithDivider(title = stringResource(id = R.string.dark_theme), - icon = Icons.Outlined.DarkMode, - isChecked = isDarkTheme, - description = LocalDarkTheme.current.getDarkThemeDesc(), - onChecked = { PreferencesUtil.modifyDarkThemePreference(if (isDarkTheme) OFF else ON) }, + SettingsSwitchWithDivider( + onCheckedChange = { + PreferencesUtil.modifyDarkThemePreference( + if (isDarkTheme) OFF else ON + ) + }, checked = isDarkTheme, + title = { + Text( + stringResource(id = R.string.dark_theme), fontWeight = FontWeight.Bold + ) + }, + description = { + Text( + LocalDarkTheme.current.getDarkThemeDesc() + ) + }, icon = Icons.Outlined.DarkMode, onClick = { navController.navigate(Route.APP_THEME) }) - if (Build.VERSION.SDK_INT >= 24) PreferenceItem( - title = stringResource(R.string.language), + + if (Build.VERSION.SDK_INT >= 24) SettingsItemNew( + title = { Text(text = stringResource(R.string.language), fontWeight = FontWeight.Bold) }, icon = Icons.Outlined.Language, - description = getLanguageDesc() - ) { navController.navigate(Route.LANGUAGES) } + description = {Text(getLanguageDesc())}, + onClick ={ navController.navigate(Route.LANGUAGES) }, + modifier = Modifier.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)) + ) } }) - if (showDarkThemeDialog) AlertDialog(onDismissRequest = { - showDarkThemeDialog = false - darkThemeValue = darkTheme.darkThemeValue - }, + if (showDarkThemeDialog) AlertDialog( + onDismissRequest = { + showDarkThemeDialog = false + darkThemeValue = darkTheme.darkThemeValue + }, confirmButton = { ConfirmButton { showDarkThemeDialog = false diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/LanguagePage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/LanguagePage.kt index 8c10dd31..ef829169 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/LanguagePage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/appearance/LanguagePage.kt @@ -1,10 +1,11 @@ package com.bobbyesp.spowlo.ui.pages.settings.appearance -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Translate import androidx.compose.material3.ExperimentalMaterial3Api @@ -19,6 +20,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource @@ -28,8 +30,8 @@ import com.bobbyesp.spowlo.MainActivity import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.LocalDarkTheme import com.bobbyesp.spowlo.ui.components.BackButton -import com.bobbyesp.spowlo.ui.components.PreferenceSingleChoiceItem import com.bobbyesp.spowlo.ui.components.PreferencesHintCard +import com.bobbyesp.spowlo.ui.components.settings.SettingsNewSingleChoiceItem import com.bobbyesp.spowlo.utils.ChromeCustomTabsUtil import com.bobbyesp.spowlo.utils.LANGUAGE import com.bobbyesp.spowlo.utils.PreferencesUtil @@ -76,6 +78,7 @@ fun LanguagePage(onBackPressed: () -> Unit) { LazyColumn( modifier = Modifier .padding(it) + .padding(horizontal = 16.dp) .selectableGroup() ) { item { @@ -87,18 +90,37 @@ fun LanguagePage(onBackPressed: () -> Unit) { ) { ChromeCustomTabsUtil.openUrl(spowloWeblateUrl) } } item { - PreferenceSingleChoiceItem( - text = stringResource(R.string.follow_system), - selected = language == SYSTEM_DEFAULT, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 18.dp) - ) { setLanguage(SYSTEM_DEFAULT) } + Column(modifier = Modifier.padding(vertical = 16.dp)) { + SettingsNewSingleChoiceItem( + text = stringResource(R.string.follow_system), + selected = language == SYSTEM_DEFAULT, + modifier = Modifier.clip(RoundedCornerShape(8.dp)) + ) { setLanguage(SYSTEM_DEFAULT) } + } + } - for (languageData in languageMap) { + for ((index, languageData) in languageMap.entries.withIndex()) { item { - PreferenceSingleChoiceItem( + SettingsNewSingleChoiceItem( text = getLanguageDesc(languageData.key), selected = language == languageData.key, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 18.dp) + modifier = when (index) { + 0 -> Modifier.clip( + RoundedCornerShape( + topStart = 8.dp, + topEnd = 8.dp + ) + ) + + languageMap.size - 1 -> Modifier.clip( + RoundedCornerShape( + bottomStart = 8.dp, + bottomEnd = 8.dp + ) + ) + + else -> Modifier + } ) { setLanguage(languageData.key) } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt index d34ae424..5f26b0ee 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/SettingsFormatsPage.kt @@ -3,6 +3,7 @@ package com.bobbyesp.spowlo.ui.pages.settings.format import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AudioFile import androidx.compose.material.icons.outlined.Audiotrack @@ -19,15 +20,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.LargeTopAppBar -import com.bobbyesp.spowlo.ui.components.PreferenceItem import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle -import com.bobbyesp.spowlo.ui.components.PreferenceSwitch +import com.bobbyesp.spowlo.ui.components.settings.SettingsItemNew +import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitch import com.bobbyesp.spowlo.utils.ORIGINAL_AUDIO import com.bobbyesp.spowlo.utils.PreferencesUtil @@ -36,8 +39,7 @@ import com.bobbyesp.spowlo.utils.PreferencesUtil fun SettingsFormatsPage(onBackPressed: () -> Unit) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState(), - canScroll = { true } - ) + canScroll = { true }) var audioFormat by remember { mutableStateOf(PreferencesUtil.getAudioFormatDesc()) } var audioQuality by remember { mutableStateOf(PreferencesUtil.getAudioQualityDesc()) } @@ -48,83 +50,101 @@ fun SettingsFormatsPage(onBackPressed: () -> Unit) { var showAudioProviderDialog by remember { mutableStateOf(false) } - Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + Scaffold(modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - LargeTopAppBar( - title = { - Text( - modifier = Modifier, - text = stringResource(id = R.string.format), fontWeight = FontWeight.Bold - ) - }, navigationIcon = { - BackButton { - onBackPressed() - } - }, scrollBehavior = scrollBehavior + LargeTopAppBar(title = { + Text( + modifier = Modifier, + text = stringResource(id = R.string.format), + fontWeight = FontWeight.Bold + ) + }, navigationIcon = { + BackButton { + onBackPressed() + } + }, scrollBehavior = scrollBehavior ) - }, content = { - LazyColumn(Modifier.padding(it)) { + }, + content = { + LazyColumn(Modifier.padding(it).padding(horizontal = 16.dp)) { item { PreferenceSubtitle(text = stringResource(id = R.string.audio)) } item { - PreferenceSwitch( - title = stringResource(id = R.string.preserve_original_audio), - description = stringResource(id = R.string.preserve_original_audio_desc), + SettingsSwitch(onCheckedChange = { + preserveOriginalAudio = !preserveOriginalAudio + PreferencesUtil.updateValue(ORIGINAL_AUDIO, preserveOriginalAudio) + }, + checked = preserveOriginalAudio, + title = { + Text( + text = stringResource(id = R.string.preserve_original_audio), + fontWeight = FontWeight.Bold + ) + }, + description = { Text(text = stringResource(id = R.string.preserve_original_audio_desc)) }, icon = Icons.Outlined.Audiotrack, - isChecked = preserveOriginalAudio, - onClick = { - preserveOriginalAudio = !preserveOriginalAudio - PreferencesUtil.updateValue(ORIGINAL_AUDIO, preserveOriginalAudio) - } + modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)) ) } item { - PreferenceItem( - title = stringResource(R.string.audio_format), - description = audioFormat, + SettingsItemNew(title = { + Text( + text = stringResource(id = R.string.audio_format), + fontWeight = FontWeight.Bold + ) + }, + description = { Text(text = audioFormat) }, icon = Icons.Outlined.AudioFile, - enabled = true, - ) { showAudioFormatDialog = true } + onClick = { showAudioFormatDialog = true }) } item { - PreferenceItem( - title = stringResource(R.string.audio_quality), - description = audioQuality, + SettingsItemNew( + title = { + Text( + text = stringResource(id = R.string.audio_quality), + fontWeight = FontWeight.Bold + ) + }, + description = { Text(text = audioQuality) }, icon = Icons.Outlined.HighQuality, + onClick = { showAudioQualityDialog = true }, enabled = !preserveOriginalAudio, - ) { showAudioQualityDialog = true } + ) } item { - PreferenceItem( - title = stringResource(R.string.audio_provider), - description = stringResource(R.string.audio_provider_desc), + SettingsItemNew(title = { + Text( + text = stringResource(id = R.string.audio_provider), + fontWeight = FontWeight.Bold + ) + }, + description = { Text(text = stringResource(id = R.string.audio_provider_desc)) }, icon = Icons.Outlined.ShuffleOn, - ) { showAudioProviderDialog = true } + onClick = { showAudioProviderDialog = true }, + modifier = Modifier.clip( + RoundedCornerShape( + bottomStart = 8.dp, bottomEnd = 8.dp + ) + ) + ) } } }) if (showAudioFormatDialog) { - AudioFormatDialog( - onDismissRequest = { showAudioFormatDialog = false } - ) { + AudioFormatDialog(onDismissRequest = { showAudioFormatDialog = false }) { audioFormat = PreferencesUtil.getAudioFormatDesc() } } if (showAudioQualityDialog) { - AudioQualityDialog( - onDismissRequest = { showAudioQualityDialog = false } - ) { + AudioQualityDialog(onDismissRequest = { showAudioQualityDialog = false }) { audioQuality = PreferencesUtil.getAudioQualityDesc() } } if (showAudioProviderDialog) { - AudioProviderDialog( - onDismissRequest = { showAudioProviderDialog = false } - ) + AudioProviderDialog(onDismissRequest = { showAudioProviderDialog = false }) } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt index 0bbc3467..d536a1a1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Construction import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.NotificationsActive import androidx.compose.material.icons.outlined.NotificationsOff @@ -35,19 +36,16 @@ import com.bobbyesp.library.SpotDLRequest import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.booleanState -import com.bobbyesp.spowlo.ui.common.intState import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.LargeTopAppBar import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle import com.bobbyesp.spowlo.ui.components.settings.ElevatedSettingsCard import com.bobbyesp.spowlo.ui.components.settings.SettingsItemNew import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitch +import com.bobbyesp.spowlo.utils.CONFIGURE import com.bobbyesp.spowlo.utils.DEBUG -import com.bobbyesp.spowlo.utils.DONT_FILTER_RESULTS -import com.bobbyesp.spowlo.utils.GEO_BYPASS import com.bobbyesp.spowlo.utils.NOTIFICATION import com.bobbyesp.spowlo.utils.PreferencesUtil -import com.bobbyesp.spowlo.utils.THREADS import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -63,9 +61,9 @@ fun GeneralSettingsPage( val scope = rememberCoroutineScope() val hapticFeedback = LocalHapticFeedback.current - val scrollBehavior = - TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState(), - canScroll = { true }) + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState(), + canScroll = { true }) var displayErrorReport by DEBUG.booleanState @@ -75,25 +73,17 @@ fun GeneralSettingsPage( ) } - var dontFilter by remember { - mutableStateOf( - PreferencesUtil.getValue(DONT_FILTER_RESULTS) - ) - } + val loadingString = App.context.getString(R.string.loading) - var useGeobypass by remember { + var spotDLVersion by remember { mutableStateOf( - PreferencesUtil.getValue(GEO_BYPASS) + loadingString ) } - var threadsNumber = THREADS.intState - - val loadingString = App.context.getString(R.string.loading) - - var spotDLVersion by remember { + var configureBeforeDownload by remember { mutableStateOf( - loadingString + PreferencesUtil.getValue(CONFIGURE) ) } @@ -112,17 +102,17 @@ fun GeneralSettingsPage( } } - Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + Scaffold(modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - LargeTopAppBar( - title = { Text(text = stringResource(id = R.string.general), fontWeight = FontWeight.Bold) }, - navigationIcon = { - BackButton { onBackPressed() } - }, - scrollBehavior = scrollBehavior + LargeTopAppBar(title = { + Text( + text = stringResource(id = R.string.general), fontWeight = FontWeight.Bold + ) + }, navigationIcon = { + BackButton { onBackPressed() } + }, scrollBehavior = scrollBehavior ) }, content = { @@ -133,8 +123,7 @@ fun GeneralSettingsPage( ) { item { ElevatedSettingsCard { - SettingsItemNew( - onClick = { }, + SettingsItemNew(onClick = { }, title = { Text( text = stringResource(id = R.string.spotdl_version), @@ -178,14 +167,41 @@ fun GeneralSettingsPage( fontWeight = FontWeight.Bold ) }, - icon = if(useNotifications) Icons.Outlined.NotificationsActive else Icons.Outlined.NotificationsOff, + icon = if (useNotifications) Icons.Outlined.NotificationsActive else Icons.Outlined.NotificationsOff, description = { Text(text = stringResource(R.string.use_notifications_desc)) }, - modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)), - ) + modifier = Modifier.clip( + RoundedCornerShape( + topStart = 8.dp, topEnd = 8.dp + ) + ), + ) + } + item { + SettingsSwitch( + onCheckedChange = { + configureBeforeDownload = !configureBeforeDownload + PreferencesUtil.updateValue(CONFIGURE, configureBeforeDownload) + }, + checked = configureBeforeDownload, + title = { + Text( + text = stringResource(R.string.pre_configure_download), + fontWeight = FontWeight.Bold + ) + }, + icon = Icons.Outlined.Construction, + description = { + Text(text = stringResource(R.string.pre_configure_download_desc)) + }, + modifier = Modifier.clip( + RoundedCornerShape( + bottomStart = 8.dp, bottomEnd = 8.dp + ) + ), + ) } - } }) } \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9189d510..549a62e0 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -5,7 +5,7 @@ Sonido Buscar actualizaciones Artista - Cambia donde quieres descargar las canciones + Cambia desde donde quieres descargar las canciones Álbum Mod AMOLED clonado Mod AMOLED @@ -22,7 +22,7 @@ Un mod de Spotify normal con saltos ilimitados, escucha en demanda, libre de anuncios y con las últimas funciones pero clonado. Esto significa que se instalará como una aplicación aparte de la original, podiendo tener ambas instaladas a la vez. Proveedor de audio Calidad de sonido - La llamada a la API de los Mods no fue bien... + La llamada a la API de los Mods no fue bien… Cancelar descarga Mira el código fuente de Spowlo en GitHub! Cambia como se ve la aplicación diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 20a8d858..d113ae1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,7 +77,7 @@ An error occurred while trying to download the song The download has finished Preserve original audio - Keep the original downloaded audio file without conversions/compression. + Keep the original downloaded audio file The best songs downloader for Android powered by spotDL. Console output Spotify client ID @@ -319,4 +319,6 @@ Parallel download Tracks Playlists + Configure download + Open the configuration dialog before every download \ No newline at end of file From f0a7456abb1b604fbb71b077a11839d1a10b3f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Tue, 4 Apr 2023 13:09:25 +0200 Subject: [PATCH 39/42] feat: Added UNSTABLE downloads cancellation and see playlist option when the URL textbox contains a playlist, updated spotdl-android --- app/build.gradle.kts | 1 + .../java/com/bobbyesp/spowlo/Downloader.kt | 4 +- .../spowlo/NotificationActionReceiver.kt | 2 +- .../download_tasks/DownloadingTaskItem.kt | 23 ++- .../spowlo/ui/components/songs/SongCard.kt | 4 +- .../songs/metadata_viewer/TrackComponent.kt | 20 +++ .../bottomsheets/DownloaderBottomSheet.kt | 112 +++++++------ .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 46 ++++-- .../pages/download_tasks/DownloadTasksPage.kt | 10 +- .../metadata_viewer/pages/PlaylistViewPage.kt | 2 + .../spowlo/ui/pages/searcher/SearcherPage.kt | 1 + .../spowlo/ui/pages/settings/SettingsPage.kt | 5 +- .../settings/format/FormatSettingsDialogs.kt | 151 +++++++++--------- .../settings/general/GeneralSettingsPage.kt | 29 +++- .../ui/pages/settings/updater/UpdaterPage.kt | 16 +- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 1 + .../com/bobbyesp/spowlo/utils/UpdateUtil.kt | 90 +++++------ app/src/main/res/values/strings.xml | 3 + gradle/libs.versions.toml | 4 +- 19 files changed, 314 insertions(+), 210 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1c63b3f8..f40f2ba0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -246,6 +246,7 @@ dependencies { // implementation(libs.exoplayer.extension.mediasession) implementation(libs.customtabs) + implementation(libs.shimmer) debugImplementation(libs.crash.handler) diff --git a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt index 8dac163e..f4ba6592 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/Downloader.kt @@ -108,7 +108,7 @@ object Downloader { fun onCancel() { toKey().run { - SpotDL.getInstance().destroyProcessById(this) + SpotDL.getInstance().destroyProcessById(this, true) onProcessCanceled(this) } } @@ -512,7 +512,7 @@ object Downloader { updateState(State.Idle) clearProgressState(isFinished = false) taskState.value.taskId.run { - SpotDL.getInstance().destroyProcessById(this) + SpotDL.getInstance().destroyProcessById(this, true) NotificationsUtil.cancelNotification(this.toNotificationId()) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt b/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt index 9ffa52f8..4caa29fb 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/NotificationActionReceiver.kt @@ -47,7 +47,7 @@ class NotificationActionReceiver : BroadcastReceiver() { private fun cancelTask(taskId: String?, notificationId: Int) { if (taskId.isNullOrEmpty()) return NotificationsUtil.cancelNotification(notificationId) - val result = SpotDL.getInstance().destroyProcessById(taskId) + val result = SpotDL.getInstance().destroyProcessById(taskId, true) NotificationsUtil.cancelNotification(notificationId) if (result) { Log.d(TAG, "Task (id:$taskId) was killed.") diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt index 29f32f76..0e6b4b83 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/download_tasks/DownloadingTaskItem.kt @@ -11,8 +11,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.outlined.Cancel import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.RestartAlt @@ -72,6 +74,7 @@ fun DownloadingTaskItem( onRestart: () -> Unit = {}, onShowLog: () -> Unit = {}, onCopyLink: () -> Unit = {}, + onCancel: () -> Unit = {} ) { CompositionLocalProvider(LocalTonalPalettes provides greenTonalPalettes) { val greenScheme = dynamicColorScheme(!LocalDarkTheme.current.isDarkTheme()) @@ -80,6 +83,7 @@ fun DownloadingTaskItem( TaskState.FINISHED -> greenScheme.primary TaskState.RUNNING -> primary TaskState.ERROR -> error.harmonizeWithPrimary() + TaskState.CANCELED -> Color.Gray.harmonizeWithPrimary() } } val containerColor = MaterialTheme.colorScheme.run { @@ -94,6 +98,7 @@ fun DownloadingTaskItem( TaskState.FINISHED -> R.string.status_completed TaskState.RUNNING -> R.string.downloading TaskState.ERROR -> R.string.error + TaskState.CANCELED -> R.string.task_canceled } ) Surface( @@ -153,6 +158,16 @@ fun DownloadingTaskItem( contentDescription = stringResource(id = R.string.searching_error) ) } + TaskState.CANCELED -> { + Icon( + modifier = Modifier + .padding(8.dp) + .size(24.dp), + imageVector = Icons.Filled.Cancel, + tint = accentColor, + contentDescription = stringResource(id = R.string.task_canceled) + ) + } } Column( @@ -234,6 +249,12 @@ fun DownloadingTaskItem( iconColor = MaterialTheme.colorScheme.secondary, ) { onRestart() } } + if (status == TaskState.RUNNING) + FlatButtonChip( + icon = Icons.Outlined.Cancel, + label = stringResource(id = R.string.cancel), + iconColor = MaterialTheme.colorScheme.secondary, + ) { onCancel() } } } } @@ -241,5 +262,5 @@ fun DownloadingTaskItem( } enum class TaskState { - FINISHED, RUNNING, ERROR + FINISHED, RUNNING, ERROR, CANCELED } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt index 65eaa63e..9a89adba 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/SongCard.kt @@ -40,13 +40,13 @@ import com.bobbyesp.spowlo.utils.GeneralTextUtils @OptIn(ExperimentalMaterial3Api::class) @Composable fun SongCard( + modifier: Modifier = Modifier, song: Song, onClick: () -> Unit = {}, progress: Float = 0.69f, isPreview: Boolean = false, isExplicit: Boolean = true, isLyrics: Boolean = false, - modifier: Modifier = Modifier, ) { Box(modifier) { ElevatedCard( @@ -174,6 +174,7 @@ fun SongCard( fun ShowSongCard() { Surface { SongCard( + song = Song( "Save Your Tears", listOf("The Weekend"), @@ -213,6 +214,7 @@ fun ShowSongCard() { fun ShowSongCardNight() { Surface { SongCard( + song = Song( "mariposas", listOf("sangiovanni"), diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt index cce8182e..4a99efd4 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -27,6 +28,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -36,6 +39,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.PopupProperties import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.common.AsyncImageImpl import com.bobbyesp.spowlo.ui.components.MarqueeText import com.bobbyesp.spowlo.ui.components.songs.ExplicitIcon import com.bobbyesp.spowlo.ui.components.songs.LyricsIcon @@ -53,6 +57,8 @@ fun TrackComponent( spotifyUrl: String, hasLyrics: Boolean = false, isExplicit: Boolean = false, + isPlaylist: Boolean = false, + imageUrl : String = "", onClick: () -> Unit = { ChromeCustomTabsUtil.openUrl(spotifyUrl) } ) { val clipboardManager = LocalClipboardManager.current @@ -72,9 +78,23 @@ fun TrackComponent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { + if(isPlaylist){ + AsyncImageImpl( + modifier = Modifier + .size(40.dp) + .aspectRatio( + 1f, matchHeightConstraintsFirst = true + ) + .clip(MaterialTheme.shapes.extraSmall), + model = imageUrl, + contentDescription = stringResource(id = R.string.track_artwork), + contentScale = ContentScale.Crop, + ) + } Column( modifier = Modifier .padding(6.dp) + .padding(start = if(isPlaylist) 6.dp else 0.dp) .weight(1f), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt index b046c111..d57e5b65 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt @@ -31,6 +31,7 @@ import androidx.compose.material.icons.outlined.DownloadDone import androidx.compose.material.icons.outlined.HighQuality import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.PlaylistAddCheck import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab @@ -85,13 +86,17 @@ import kotlinx.coroutines.launch fun DownloaderBottomSheet( onBackPressed: () -> Unit, downloaderViewModel: DownloaderViewModel, - navController: NavController + navController: NavController, + navigateToPlaylist: (String) -> Unit ) { val scope = rememberCoroutineScope() val pagerState = rememberPagerState(initialPage = 0) - val pages = listOf(BottomSheetPages.MAIN, BottomSheetPages.SECONDARY, BottomSheetPages.TERTIARY) + val pages = + listOf(BottomSheetPages.MAIN, BottomSheetPages.TERTIARY) //, BottomSheetPages.SECONDARY + val viewState by downloaderViewModel.viewStateFlow.collectAsStateWithLifecycle() + val roundedTopShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 0.dp, bottomEnd = 0.dp) @@ -202,8 +207,7 @@ fun DownloaderBottomSheet( .padding(8.dp) .animateContentSize( animationSpec = tween( - durationMillis = 300, - easing = FastOutSlowInEasing + durationMillis = 300, easing = FastOutSlowInEasing ), ) @@ -222,9 +226,8 @@ fun DownloaderBottomSheet( Text( text = stringResource(R.string.settings_before_download), style = MaterialTheme.typography.headlineSmall, - modifier = Modifier - .padding(vertical = 12.dp), - maxLines = 2, + modifier = Modifier.padding(vertical = 12.dp), + maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center, @@ -237,7 +240,9 @@ fun DownloaderBottomSheet( fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.Start).padding(start = 8.dp) + modifier = Modifier + .align(Alignment.Start) + .padding(start = 8.dp) ) IndicatorBehindScrollableTabRow( selectedTabIndex = pagerState.currentPage, @@ -267,11 +272,15 @@ fun DownloaderBottomSheet( ) } } - HorizontalPager(pageCount = pages.size, state = pagerState, modifier = Modifier.animateContentSize()) { + HorizontalPager( + pageCount = pages.size, state = pagerState, modifier = Modifier.animateContentSize() + ) { when (pages[it]) { BottomSheetPages.MAIN -> { Column( - modifier = Modifier.fillMaxWidth().padding(6.dp) + modifier = Modifier + .fillMaxWidth() + .padding(6.dp) ) { DrawerSheetSubtitle(text = stringResource(id = R.string.general)) Row( @@ -283,8 +292,7 @@ fun DownloaderBottomSheet( color = MaterialTheme.colorScheme.surfaceVariant ), ) { - AudioFilterChip( - label = stringResource(id = R.string.preserve_original_audio), + AudioFilterChip(label = stringResource(id = R.string.preserve_original_audio), animated = true, selected = preserveOriginalAudio, onClick = { @@ -292,8 +300,7 @@ fun DownloaderBottomSheet( scope.launch { settings.updateValue(ORIGINAL_AUDIO, preserveOriginalAudio) } - } - ) + }) ButtonChip( label = stringResource(id = R.string.audio_format), icon = Icons.Outlined.AudioFile, @@ -316,17 +323,17 @@ fun DownloaderBottomSheet( color = MaterialTheme.colorScheme.surfaceVariant ), ) { - AudioFilterChip( - label = stringResource(id = R.string.use_spotify_credentials), + AudioFilterChip(label = stringResource(id = R.string.use_spotify_credentials), animated = true, selected = useSpotifyCredentials, onClick = { useSpotifyCredentials = !useSpotifyCredentials scope.launch { - settings.updateValue(USE_SPOTIFY_CREDENTIALS, useSpotifyCredentials) + settings.updateValue( + USE_SPOTIFY_CREDENTIALS, useSpotifyCredentials + ) } - } - ) + }) ButtonChip( label = stringResource(id = R.string.client_id), icon = Icons.Outlined.Person, @@ -350,7 +357,9 @@ fun DownloaderBottomSheet( BottomSheetPages.TERTIARY -> { Column( - modifier = Modifier.fillMaxWidth().padding(6.dp) + modifier = Modifier + .fillMaxWidth() + .padding(6.dp) ) { DrawerSheetSubtitle(text = stringResource(id = R.string.experimental_features)) Row( @@ -362,8 +371,7 @@ fun DownloaderBottomSheet( color = MaterialTheme.colorScheme.surfaceVariant ), ) { - AudioFilterChip( - label = stringResource(id = R.string.synced_lyrics), + AudioFilterChip(label = stringResource(id = R.string.synced_lyrics), animated = true, selected = useSyncedLyrics, onClick = { @@ -371,10 +379,8 @@ fun DownloaderBottomSheet( scope.launch { settings.updateValue(SYNCED_LYRICS, useSyncedLyrics) } - } - ) - AudioFilterChip( - label = stringResource(id = R.string.geo_bypass), + }) + AudioFilterChip(label = stringResource(id = R.string.geo_bypass), selected = useGeoBypass, animated = true, onClick = { @@ -382,11 +388,9 @@ fun DownloaderBottomSheet( scope.launch { settings.updateValue(GEO_BYPASS, useGeoBypass) } - } - ) + }) - AudioFilterChip( - label = stringResource(id = R.string.use_cache), + AudioFilterChip(label = stringResource(id = R.string.use_cache), animated = true, selected = useCaching, onClick = { @@ -394,11 +398,9 @@ fun DownloaderBottomSheet( scope.launch { settings.updateValue(USE_CACHING, useCaching) } - } - ) + }) - AudioFilterChip( - label = stringResource(id = R.string.dont_filter_results), + AudioFilterChip(label = stringResource(id = R.string.dont_filter_results), selected = dontFilter, animated = true, onClick = { @@ -406,10 +408,8 @@ fun DownloaderBottomSheet( scope.launch { settings.updateValue(DONT_FILTER_RESULTS, dontFilter) } - } - ) - AudioFilterChip( - label = stringResource(id = R.string.use_cookies), + }) + AudioFilterChip(label = stringResource(id = R.string.use_cookies), animated = true, selected = useCookies, onClick = { @@ -417,10 +417,8 @@ fun DownloaderBottomSheet( scope.launch { settings.updateValue(COOKIES, useCookies) } - } - ) - AudioFilterChip( - label = stringResource(id = R.string.use_yt_metadata), + }) + AudioFilterChip(label = stringResource(id = R.string.use_yt_metadata), animated = true, selected = useYtMetadata, onClick = { @@ -428,8 +426,7 @@ fun DownloaderBottomSheet( scope.launch { settings.updateValue(USE_YT_METADATA, useYtMetadata) } - } - ) + }) } } } @@ -466,11 +463,28 @@ fun DownloaderBottomSheet( ) } item { - FilledButtonWithIcon( - onClick = downloadButtonCallback, - icon = Icons.Outlined.DownloadDone, - text = stringResource(R.string.start_download) - ) + if (viewState.url.contains("playlist")) { + //https://open.spotify.com/playlist/4aKFWQtn0Tstw68SIMURye?si=c9e7282b0c354d34 + //get playlist id after the playlist/ and before the ? + var playlistId = viewState.url.substringAfter("playlist/").substringBefore("?") + + if (viewState.url == "playlist") + run { + playlistId = "7804lpXmApCGPd2Rdai6k1" + } + FilledButtonWithIcon( + onClick = { navigateToPlaylist(playlistId) }, + icon = Icons.Outlined.PlaylistAddCheck, + text = stringResource(R.string.see_playlist) + ) + + } else { + FilledButtonWithIcon( + onClick = downloadButtonCallback, + icon = Icons.Outlined.DownloadDone, + text = stringResource(R.string.start_download) + ) + } } } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index 6ea3e936..c4e5a126 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar @@ -51,6 +52,7 @@ import androidx.navigation.compose.dialog import androidx.navigation.navArgument import androidx.navigation.navDeepLink import androidx.navigation.navigation +import com.bobbyesp.library.SpotDL import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.MainActivity import com.bobbyesp.spowlo.R @@ -95,6 +97,8 @@ import com.bobbyesp.spowlo.ui.pages.settings.general.GeneralSettingsPage import com.bobbyesp.spowlo.ui.pages.settings.spotify.SpotifySettingsPage import com.bobbyesp.spowlo.ui.pages.settings.updater.UpdaterPage import com.bobbyesp.spowlo.utils.PreferencesUtil +import com.bobbyesp.spowlo.utils.PreferencesUtil.getString +import com.bobbyesp.spowlo.utils.SPOTDL import com.bobbyesp.spowlo.utils.ToastUtil import com.bobbyesp.spowlo.utils.UpdateUtil import com.google.accompanist.navigation.animation.AnimatedNavHost @@ -106,12 +110,13 @@ import com.google.accompanist.navigation.material.rememberBottomSheetNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext private const val TAG = "InitialEntry" @OptIn( ExperimentalAnimationApi::class, ExperimentalMaterialNavigationApi::class, - ExperimentalLayoutApi::class + ExperimentalLayoutApi::class, ExperimentalMaterialApi::class ) @Composable fun InitialEntry( @@ -423,7 +428,6 @@ fun InitialEntry( } //DIALOGS ------------------------------- - //TODO: ADD DIALOGS dialog(Route.AUDIO_QUALITY_DIALOG) { AudioQualityDialog( onBackPressed @@ -439,13 +443,19 @@ fun InitialEntry( } bottomSheet(Route.DOWNLOADER_SHEET) { - DownloaderBottomSheet( - onBackPressed, - downloaderViewModel, - navController - ) - } + /*ModalBottomSheetLayout( + bottomSheetNavigator = BottomSheetNavigator( + rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden, skipHalfExpanded = true), + ) + ) {*/ + DownloaderBottomSheet( + onBackPressed, + downloaderViewModel, + navController + ) { id -> navController.navigate(Route.PLAYLIST_PAGE + "/" + "playlist" + "/" + id) } + // } + } } //Can add the downloads history bottom sheet here using `val downloadsHistoryViewModel = hiltViewModel()` @@ -500,7 +510,10 @@ fun InitialEntry( } } - navigation(startDestination = Route.DOWNLOAD_TASKS, route = Route.DownloadTasksNavi) { + navigation( + startDestination = Route.DOWNLOAD_TASKS, + route = Route.DownloadTasksNavi + ) { animatedComposable( Route.FULLSCREEN_LOG arg "taskHashCode", @@ -536,7 +549,6 @@ fun InitialEntry( } } - LaunchedEffect(Unit) { if (PreferencesUtil.isNetworkAvailable()) launch(Dispatchers.IO) { runCatching { @@ -565,6 +577,20 @@ fun InitialEntry( } } + LaunchedEffect(Unit) { + if (SPOTDL.getString().isNotEmpty()) return@LaunchedEffect + kotlin.runCatching { + withContext(Dispatchers.IO) { + val result = UpdateUtil.updateSpotDL() + if (result == SpotDL.UpdateStatus.DONE) { + ToastUtil.makeToastSuspend( + App.context.getString(R.string.spotdl_update_success) + .format(SPOTDL.getString()) + ) + } + } + } + } if (showUpdateDialog) { UpdaterBottomDrawer(latestRelease = latestRelease) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt index cd1a1a5b..233558a4 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/download_tasks/DownloadTasksPage.kt @@ -67,6 +67,9 @@ fun DownloadTasksPage(onNavigateToDetail: (Int) -> Unit) { onCopyError = { onCopyError(clipboardManager) }, + onCancel = { + onCancel() + }, onRestart = { onRestart() }, onCopyLog = { @@ -96,7 +99,12 @@ fun DownloadTasksPage(onNavigateToDetail: (Int) -> Unit) { style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) - HorizontalDivider( modifier = Modifier.padding(vertical = 24.dp, horizontal = 4.dp)) + HorizontalDivider( + modifier = Modifier.padding( + vertical = 24.dp, + horizontal = 4.dp + ) + ) Text( text = stringResource(R.string.no_running_downloads_description), style = MaterialTheme.typography.labelLarge, diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index 4be7dfd2..fad6f3d8 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -146,6 +146,8 @@ fun PlaylistViewPage( artists = actualTrack?.artists?.joinToString(", ") { it.name } ?: "", spotifyUrl = actualTrack?.externalUrls?.spotify ?: "", isExplicit = actualTrack?.explicit ?: false, + isPlaylist = true, + imageUrl = actualTrack?.album?.images?.get(0)?.url ?: "", onClick = { if (actualTrack != null) { trackDownloadCallback(actualTrack.externalUrls.spotify!!, taskName) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index d6fcb2ce..17447d25 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -76,6 +76,7 @@ fun SearcherPage( ) { val viewState by searcherPageViewModel.viewStateFlow.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() + Box( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt index 65b9279e..aaf439ce 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/SettingsPage.kt @@ -25,7 +25,6 @@ import androidx.compose.material.icons.filled.AudioFile import androidx.compose.material.icons.filled.Cookie import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Folder -import androidx.compose.material.icons.filled.Help import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.SettingsApplications import androidx.compose.material.icons.filled.Update @@ -271,7 +270,7 @@ fun SettingsPage(navController: NavController) { ) } - item { + /*item { SettingsItemNew( title = { Text( @@ -290,7 +289,7 @@ fun SettingsPage(navController: NavController) { highlightIcon = true ) } - +*/ item { SettingsItemNew( title = { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/FormatSettingsDialogs.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/FormatSettingsDialogs.kt index fb592b0f..38a32306 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/FormatSettingsDialogs.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/format/FormatSettingsDialogs.kt @@ -1,5 +1,6 @@ package com.bobbyesp.spowlo.ui.pages.settings.format +import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -32,89 +33,83 @@ import com.bobbyesp.spowlo.utils.PreferencesUtil @Composable fun AudioFormatDialog(onDismissRequest: () -> Unit, onConfirm: () -> Unit = {}) { var audioFormat by remember { mutableStateOf(PreferencesUtil.getAudioFormat()) } - AlertDialog( - onDismissRequest = onDismissRequest, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(stringResource(R.string.dismiss)) - } - }, - icon = { Icon(Icons.Outlined.AudioFile, null) }, - title = { - Text(stringResource(R.string.audio_format)) - }, confirmButton = { - TextButton(onClick = { - PreferencesUtil.encodeInt(AUDIO_FORMAT, audioFormat) - onConfirm() - onDismissRequest() - }) { - Text(text = stringResource(R.string.confirm)) - } - }, text = { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), - text = stringResource(R.string.audio_format_desc), - style = MaterialTheme.typography.bodyLarge - ) - for (i in 0..5) - SingleChoiceItem( - text = PreferencesUtil.getAudioFormatDesc(i), - selected = audioFormat == i - ) { audioFormat = i } - } - }) + AlertDialog(onDismissRequest = onDismissRequest, dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.dismiss)) + } + }, icon = { Icon(Icons.Outlined.AudioFile, null) }, title = { + Text(stringResource(R.string.audio_format)) + }, confirmButton = { + TextButton(onClick = { + PreferencesUtil.encodeInt(AUDIO_FORMAT, audioFormat) + onConfirm() + onDismissRequest() + }) { + Text(text = stringResource(R.string.confirm)) + } + }, text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + text = stringResource(R.string.audio_format_desc), + style = MaterialTheme.typography.bodyLarge + ) + for (i in 0..5) SingleChoiceItem( + text = PreferencesUtil.getAudioFormatDesc(i), selected = audioFormat == i + ) { audioFormat = i } + } + }) } @Composable fun AudioQualityDialog(onDismissRequest: () -> Unit, onConfirm: () -> Unit = {}) { var audioQuality by remember { mutableStateOf(PreferencesUtil.getAudioQuality()) } - AlertDialog( - onDismissRequest = onDismissRequest, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(stringResource(R.string.dismiss)) - } - }, - icon = { Icon(Icons.Outlined.HighQuality, null) }, - title = { - Text(stringResource(R.string.audio_quality)) - }, confirmButton = { - TextButton(onClick = { - PreferencesUtil.encodeInt(AUDIO_QUALITY, audioQuality) - onConfirm() - onDismissRequest() - }) { - Text(text = stringResource(R.string.confirm)) - } - }, text = { - Column(modifier = Modifier) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), - text = stringResource(R.string.audio_quality_desc), - style = MaterialTheme.typography.bodyLarge - ) - LazyColumn(content = { - for (i in 0..17) - item { - SingleChoiceItem( - text = PreferencesUtil.getAudioQualityDesc(i), - selected = audioQuality == i - ) { audioQuality = i } + AlertDialog(onDismissRequest = onDismissRequest, dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.dismiss)) + } + }, icon = { Icon(Icons.Outlined.HighQuality, null) }, title = { + Text(stringResource(R.string.audio_quality)) + }, confirmButton = { + TextButton(onClick = { + Log.d("FormatSettingsDialog", "The chosen audioQuality is: $audioQuality") + PreferencesUtil.encodeInt(AUDIO_QUALITY, audioQuality) + Log.d( + "FormatSettingsDialog", + "The encoded int to the AUDIO_QUALITY settings var is: ${PreferencesUtil.getAudioQuality()}" + ) + onConfirm() + onDismissRequest() + }) { + Text(text = stringResource(R.string.confirm)) + } + }, text = { + Column(modifier = Modifier) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + text = stringResource(R.string.audio_quality_desc), + style = MaterialTheme.typography.bodyLarge + ) + LazyColumn( + content = { + for (i in 17 downTo 0) item { + SingleChoiceItem( + text = PreferencesUtil.getAudioQualityDesc(i), + selected = audioQuality == i + ) { + audioQuality = i + Log.d( + "FormatSettingsDialog", "Changed to $i" + ) } - }, modifier = Modifier.size(400.dp)) - } - }) -} - -@Composable -fun CustomOutputBottomDrawer( - -){ - + } + }, modifier = Modifier.size(400.dp) + ) + } + }) } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt index d536a1a1..9ddeb389 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/general/GeneralSettingsPage.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.bobbyesp.library.SpotDL -import com.bobbyesp.library.SpotDLRequest import com.bobbyesp.spowlo.App import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.common.booleanState @@ -42,10 +41,15 @@ import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle import com.bobbyesp.spowlo.ui.components.settings.ElevatedSettingsCard import com.bobbyesp.spowlo.ui.components.settings.SettingsItemNew import com.bobbyesp.spowlo.ui.components.settings.SettingsSwitch +import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.getString import com.bobbyesp.spowlo.utils.CONFIGURE import com.bobbyesp.spowlo.utils.DEBUG import com.bobbyesp.spowlo.utils.NOTIFICATION import com.bobbyesp.spowlo.utils.PreferencesUtil +import com.bobbyesp.spowlo.utils.PreferencesUtil.getString +import com.bobbyesp.spowlo.utils.SPOTDL +import com.bobbyesp.spowlo.utils.ToastUtil +import com.bobbyesp.spowlo.utils.UpdateUtil import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -72,6 +76,7 @@ fun GeneralSettingsPage( PreferencesUtil.getValue(NOTIFICATION) ) } + var isUpdatingLib by remember { mutableStateOf(false) } val loadingString = App.context.getString(R.string.loading) @@ -92,8 +97,8 @@ fun GeneralSettingsPage( GlobalScope.launch { try { withContext(Dispatchers.IO) { - spotDLVersion = SpotDL.getInstance() - .execute(SpotDLRequest().addOption("-v"), null, null).output + spotDLVersion = SpotDL.getInstance().version(appContext = App.context) + ?: getString(R.string.unknown) } } catch (e: Exception) { spotDLVersion = e.message ?: e.toString() @@ -123,7 +128,23 @@ fun GeneralSettingsPage( ) { item { ElevatedSettingsCard { - SettingsItemNew(onClick = { }, + SettingsItemNew( + onClick = { + scope.launch { + runCatching { + isUpdatingLib = true + UpdateUtil.updateSpotDL() + spotDLVersion = SPOTDL.getString() + }.onFailure { + ToastUtil.makeToastSuspend(App.context.getString(R.string.spotdl_update_failed)) + }.onSuccess { + ToastUtil.makeToastSuspend( + App.context.getString(R.string.spotdl_update_success) + .format(spotDLVersion) + ) + } + } + }, title = { Text( text = stringResource(id = R.string.spotdl_version), diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/updater/UpdaterPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/updater/UpdaterPage.kt index 4af77ad9..ec22e63c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/updater/UpdaterPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/settings/updater/UpdaterPage.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Update import androidx.compose.material3.ButtonDefaults @@ -29,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -41,9 +43,9 @@ import com.bobbyesp.spowlo.ui.common.intState import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.PreferenceInfo -import com.bobbyesp.spowlo.ui.components.PreferenceSingleChoiceItem import com.bobbyesp.spowlo.ui.components.PreferenceSubtitle import com.bobbyesp.spowlo.ui.components.PreferenceSwitchWithContainer +import com.bobbyesp.spowlo.ui.components.settings.SettingsNewSingleChoiceItem import com.bobbyesp.spowlo.ui.dialogs.UpdateDialog import com.bobbyesp.spowlo.utils.AUTO_UPDATE import com.bobbyesp.spowlo.utils.PRE_RELEASE @@ -105,10 +107,11 @@ fun UpdaterPage(onBackPressed: () -> Unit) { ) } item { - PreferenceSingleChoiceItem( + SettingsNewSingleChoiceItem( text = stringResource(id = R.string.stable_channel), selected = updateChannel == STABLE, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp) + contentPadding = PaddingValues(horizontal = 12.dp), + modifier = Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)) ) { updateChannel = STABLE UPDATE_CHANNEL.updateInt(updateChannel) @@ -116,10 +119,11 @@ fun UpdaterPage(onBackPressed: () -> Unit) { } item { - PreferenceSingleChoiceItem( + SettingsNewSingleChoiceItem( text = stringResource(id = R.string.pre_release_channel), selected = updateChannel == PRE_RELEASE, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp) + contentPadding = PaddingValues(horizontal = 12.dp), + modifier = Modifier.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)) ) { updateChannel = PRE_RELEASE UPDATE_CHANNEL.updateInt(updateChannel) @@ -129,7 +133,7 @@ fun UpdaterPage(onBackPressed: () -> Unit) { var isLoading by remember { mutableStateOf(false) } Row( horizontalArrangement = Arrangement.End, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(top = 12.dp) ) { ProgressIndicatorButton( modifier = Modifier diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index aca672f0..2ab1c822 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -437,6 +437,7 @@ object DownloaderUtil { val finalResponse = removeDuplicateLines(clearLinesWithEllipsis(response.output)) onTaskEnded(url, finalResponse, name) }.onFailure { + Log.d("Canceled?", "Exception: $it") it.printStackTrace() ToastUtil.makeToastSuspend(context.getString(R.string.download_error_msg)) if (it is SpotDL.CanceledException) return@onFailure diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/UpdateUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/UpdateUtil.kt index 9f1fdc39..e21359f1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/UpdateUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/UpdateUtil.kt @@ -53,13 +53,13 @@ object UpdateUtil { private val _updateViewState = MutableStateFlow(UpdateViewState()) val updateViewState = _updateViewState.asStateFlow() - fun showUpdateDrawer(){ + fun showUpdateDrawer() { _updateViewState.update { it.copy(drawerState = ModalBottomSheetState(ModalBottomSheetValue.Expanded)) } } - fun hideUpdateDrawer(){ + fun hideUpdateDrawer() { _updateViewState.update { it.copy(drawerState = ModalBottomSheetState(ModalBottomSheetValue.Hidden)) } @@ -81,48 +81,44 @@ object UpdateUtil { .build() private val requestForReleases = - Request.Builder().url("https://api.github.com/repos/${OWNER}/${REPO}/releases") - .build() + Request.Builder().url("https://api.github.com/repos/${OWNER}/${REPO}/releases").build() private val jsonFormat = Json { ignoreUnknownKeys = true } - suspend fun updateSpotDL(): SpotDL.UpdateStatus? = - withContext(Dispatchers.IO) { - SpotDL.getInstance().updateSpotDL( - context, - "https://api.github.com/repos/spotDL/spotify-downloader/releases/latest" - ).apply { - if (this == SpotDL.UpdateStatus.DONE) - SpotDL.getInstance().version(context)?.let { - PreferencesUtil.encodeString(SPOTDL, it) - } + suspend fun updateSpotDL(): SpotDL.UpdateStatus? = withContext(Dispatchers.IO) { + SpotDL.getInstance().updateSpotDL( + context + ).apply { + if (this == SpotDL.UpdateStatus.DONE) SpotDL.getInstance().version(context)?.let { + PreferencesUtil.encodeString(SPOTDL, it) } } + } private suspend fun getLatestRelease(): LatestRelease { return suspendCoroutine { continuation -> - client.newCall(requestForReleases) - .enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - val responseData = response.body.string() + client.newCall(requestForReleases).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val responseData = response.body.string() // val latestRelease = jsonFormat.decodeFromString(responseData) - val releaseList = - jsonFormat.decodeFromString>(responseData) - val latestRelease = - releaseList.filter { if (UPDATE_CHANNEL.getInt() == STABLE) it.name.toVersion() is Version.Stable else true } - .maxByOrNull { it.name.toVersion() } - ?: throw Exception("null response") - releaseList.sortedBy { it.name.toVersion() }.forEach { - Log.d(TAG, it.tagName.toString()) + val releaseList = + jsonFormat.decodeFromString>(responseData) + val latestRelease = + releaseList.filter { if (UPDATE_CHANNEL.getInt() == STABLE) it.name.toVersion() is Version.Stable else true } + .maxByOrNull { it.name.toVersion() } + ?: throw Exception("null response") + releaseList.sortedBy { it.name.toVersion() }.forEach { + Log.d(TAG, it.tagName.toString()) + } + response.body.close() + continuation.resume(latestRelease) } - response.body.close() - continuation.resume(latestRelease) - } - override fun onFailure(call: Call, e: IOException) { - continuation.resumeWithException(e) - } - }) + + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + }) } } @@ -146,8 +142,7 @@ object UpdateUtil { } - private fun Context.getLatestApk() = - File(getExternalFilesDir("apk"), "latest.apk") + private fun Context.getLatestApk() = File(getExternalFilesDir("apk"), "latest.apk") private fun Context.getFileProvider() = "${packageName}.provider" @@ -167,8 +162,7 @@ object UpdateUtil { } suspend fun downloadApk( - context: Context = App.context, - latestRelease: LatestRelease + context: Context = App.context, latestRelease: LatestRelease ): Flow = withContext(Dispatchers.IO) { val apkVersion = context.packageManager.getPackageArchiveInfo( context.getLatestApk().absolutePath, 0 @@ -289,10 +283,7 @@ object UpdateUtil { sealed class Version( - val major: Int, - val minor: Int, - val patch: Int, - val build: Int = 0 + val major: Int, val minor: Int, val patch: Int, val build: Int = 0 ) : Comparable { companion object { private const val BUILD = 1L @@ -306,8 +297,7 @@ object UpdateUtil { class Beta(versionMajor: Int, versionMinor: Int, versionPatch: Int, versionBuild: Int) : Version(versionMajor, versionMinor, versionPatch, versionBuild) { - override fun toVersionName(): String = - "${major}.${minor}.${patch}-beta.$build" + override fun toVersionName(): String = "${major}.${minor}.${patch}-beta.$build" override fun toNumber(): Long = major * MAJOR + minor * MINOR + patch * PATCH + build * BUILD @@ -316,8 +306,7 @@ object UpdateUtil { class Stable(versionMajor: Int = 0, versionMinor: Int = 0, versionPatch: Int = 0) : Version(versionMajor, versionMinor, versionPatch) { - override fun toVersionName(): String = - "${major}.${minor}.${patch}" + override fun toVersionName(): String = "${major}.${minor}.${patch}" override fun toNumber(): Long = major * MAJOR + minor * MINOR + patch * PATCH + build * BUILD + 50 @@ -326,14 +315,9 @@ object UpdateUtil { } class ReleaseCandidate( - versionMajor: Int, - versionMinor: Int, - versionPatch: Int, - versionBuild: Int - ) : - Version(versionMajor, versionMinor, versionPatch, versionBuild) { - override fun toVersionName(): String = - "${major}.${minor}.${patch}-rc.$build" + versionMajor: Int, versionMinor: Int, versionPatch: Int, versionBuild: Int + ) : Version(versionMajor, versionMinor, versionPatch, versionBuild) { + override fun toVersionName(): String = "${major}.${minor}.${patch}-rc.$build" override fun toNumber(): Long = major * MAJOR + minor * MINOR + patch * PATCH + build * BUILD + 25 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d113ae1c..681bc33d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -321,4 +321,7 @@ Playlists Configure download Open the configuration dialog before every download + The spotDL update failed. + SpotDL is updated. You are using the version %1$s + See playlist \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5973ca4d..602db51c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,9 +21,10 @@ androidxHiltNavigationCompose = "1.0.0" androidxTestExt = "1.1.5" -spotdlAndroidVersion = "dd296e76cc" +spotdlAndroidVersion = "3d8e42a3f0" spotifyApiKotlinVersion = "3.8.8" +shimmerLibrary = "1.0.4" crashHandlerVersion = "2.0.2" coil = "2.2.2" @@ -125,6 +126,7 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = mmkv = { group = "com.tencent", name = "mmkv", version.ref = "mmkv" } crash-handler = { group = "com.github.thelumiereguy", name = "CrashWatcher-Android", version.ref = "crashHandlerVersion" } +shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", version.ref = "shimmerLibrary" } soup-anims-core = { group = "io.github.fornewid", name = "material-motion-compose-core", version.ref = "navTransitions" } soup-anims-navigation = { group = "io.github.fornewid", name = "material-motion-compose-navigation", version.ref = "navTransitions" } From 1acb6e6ad1b376116fa97b7e299b0b05118d3d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Wed, 5 Apr 2023 00:55:39 +0200 Subject: [PATCH 40/42] bugfix & stability: Improved by a little the searching page and also the track, playlist and added albums support. --- app/build.gradle.kts | 4 +- app/src/main/java/com/bobbyesp/spowlo/App.kt | 1 + .../songs/metadata_viewer/TrackComponent.kt | 2 +- .../bottomsheets/DownloaderBottomSheet.kt | 35 ++- .../{common => commonPages}/ErrorPage.kt | 2 +- .../{common => commonPages}/LoadingPage.kt | 2 +- .../NotImplementedPage.kt | 2 +- .../ui/pages/history/DownloadsHistoryPage.kt | 216 +++++++++--------- .../binders/SpotifyPageBinder.kt | 40 ++-- .../pages/metadata_viewer/pages/AlbumPage.kt | 149 +++++++++++- .../pages/metadata_viewer/pages/ArtistPage.kt | 2 +- .../metadata_viewer/pages/PlaylistViewPage.kt | 76 +++--- .../pages/metadata_viewer/pages/TrackPage.kt | 50 ++-- .../metadata_viewer/playlists/PlaylistPage.kt | 78 +++---- .../playlists/PlaylistPageViewModel.kt | 9 +- .../spowlo/ui/pages/searcher/SearcherPage.kt | 83 ++++++- .../bobbyesp/spowlo/utils/DownloaderUtil.kt | 1 - app/src/main/res/values/strings.xml | 2 + 18 files changed, 501 insertions(+), 253 deletions(-) rename app/src/main/java/com/bobbyesp/spowlo/ui/pages/{common => commonPages}/ErrorPage.kt (97%) rename app/src/main/java/com/bobbyesp/spowlo/ui/pages/{common => commonPages}/LoadingPage.kt (96%) rename app/src/main/java/com/bobbyesp/spowlo/ui/pages/{common => commonPages}/NotImplementedPage.kt (96%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f40f2ba0..f92df50f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,7 +71,7 @@ android { applicationId = "com.bobbyesp.spowlo" minSdk = 26 targetSdk = 33 - versionCode = 10201 + versionCode = 10300 versionName = currentVersion.toVersionName().run { if (!splitApks) "$this-(F-Droid)" @@ -246,7 +246,7 @@ dependencies { // implementation(libs.exoplayer.extension.mediasession) implementation(libs.customtabs) - implementation(libs.shimmer) + // implementation(libs.shimmer) debugImplementation(libs.crash.handler) diff --git a/app/src/main/java/com/bobbyesp/spowlo/App.kt b/app/src/main/java/com/bobbyesp/spowlo/App.kt index bbb3c551..f35eec68 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/App.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/App.kt @@ -103,6 +103,7 @@ class App : Application() { } override fun onServiceDisconnected(arg0: ComponentName) { + } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt index 4a99efd4..9316a69d 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/components/songs/metadata_viewer/TrackComponent.kt @@ -78,7 +78,7 @@ fun TrackComponent( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - if(isPlaylist){ + if(isPlaylist && imageUrl.isNotEmpty()) { AsyncImageImpl( modifier = Modifier .size(40.dp) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt index d57e5b65..0bf51f3e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/dialogs/bottomsheets/DownloaderBottomSheet.kt @@ -361,6 +361,28 @@ fun DownloaderBottomSheet( .fillMaxWidth() .padding(6.dp) ) { + DrawerSheetSubtitle(text = stringResource(id = R.string.general)) + Row( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + .fillMaxWidth() + .clip(RoundedCornerShape(6.dp)) + .background( + color = MaterialTheme.colorScheme.surfaceVariant + ), + ) { + AudioFilterChip(label = stringResource(id = R.string.use_cache), + animated = true, + selected = useCaching, + onClick = { + useCaching = !useCaching + scope.launch { + settings.updateValue(USE_CACHING, useCaching) + } + }) + + } + DrawerSheetSubtitle(text = stringResource(id = R.string.experimental_features)) Row( modifier = Modifier @@ -389,17 +411,6 @@ fun DownloaderBottomSheet( settings.updateValue(GEO_BYPASS, useGeoBypass) } }) - - AudioFilterChip(label = stringResource(id = R.string.use_cache), - animated = true, - selected = useCaching, - onClick = { - useCaching = !useCaching - scope.launch { - settings.updateValue(USE_CACHING, useCaching) - } - }) - AudioFilterChip(label = stringResource(id = R.string.dont_filter_results), selected = dontFilter, animated = true, @@ -514,7 +525,7 @@ fun DownloaderBottomSheet( object BottomSheetPages { val MAIN = getString(R.string.audio) val SECONDARY = "secondary" - val TERTIARY = getString(R.string.experimental_features) + val TERTIARY = getString(R.string.downloader) } //GET STRING FROM APP.CONTEXT GIVEN A r.string ID diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/commonPages/ErrorPage.kt similarity index 97% rename from app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt rename to app/src/main/java/com/bobbyesp/spowlo/ui/pages/commonPages/ErrorPage.kt index 38e4efe6..4be71eb0 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/ErrorPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/commonPages/ErrorPage.kt @@ -1,4 +1,4 @@ -package com.bobbyesp.spowlo.ui.pages.common +package com.bobbyesp.spowlo.ui.pages.commonPages import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/LoadingPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/commonPages/LoadingPage.kt similarity index 96% rename from app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/LoadingPage.kt rename to app/src/main/java/com/bobbyesp/spowlo/ui/pages/commonPages/LoadingPage.kt index 9a875e24..de1c6b4b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/LoadingPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/commonPages/LoadingPage.kt @@ -1,4 +1,4 @@ -package com.bobbyesp.spowlo.ui.pages.common +package com.bobbyesp.spowlo.ui.pages.commonPages import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/NotImplementedPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/commonPages/NotImplementedPage.kt similarity index 96% rename from app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/NotImplementedPage.kt rename to app/src/main/java/com/bobbyesp/spowlo/ui/pages/commonPages/NotImplementedPage.kt index 1b77752c..020786fc 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/common/NotImplementedPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/commonPages/NotImplementedPage.kt @@ -1,4 +1,4 @@ -package com.bobbyesp.spowlo.ui.pages.common +package com.bobbyesp.spowlo.ui.pages.commonPages import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadsHistoryPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadsHistoryPage.kt index 6dacaff1..790088ae 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadsHistoryPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/history/DownloadsHistoryPage.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -25,9 +24,9 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DownloadForOffline import androidx.compose.material.icons.outlined.Checklist import androidx.compose.material.icons.outlined.DeleteSweep -import androidx.compose.material.icons.outlined.DownloadForOffline import androidx.compose.material3.BottomAppBar import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api @@ -58,6 +57,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -69,6 +69,7 @@ import com.bobbyesp.spowlo.ui.components.AudioFilterChip import com.bobbyesp.spowlo.ui.components.BackButton import com.bobbyesp.spowlo.ui.components.ConfirmButton import com.bobbyesp.spowlo.ui.components.DismissButton +import com.bobbyesp.spowlo.ui.components.HorizontalDivider import com.bobbyesp.spowlo.ui.components.LargeTopAppBar import com.bobbyesp.spowlo.ui.components.MultiChoiceItem import com.bobbyesp.spowlo.ui.components.SpowloDialog @@ -102,10 +103,9 @@ fun DownloadsHistoryPage( Log.d("DownloadsHistoryPage", songsList.toString()) } - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - rememberTopAppBarState(), - canScroll = { true } - ) + val scrollBehavior = + TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState(), + canScroll = { true }) val scope = rememberCoroutineScope() val fileSizeMap = remember(songsList.size) { @@ -158,11 +158,9 @@ fun DownloadsHistoryPage( remember(songsList, isSelectEnabled, viewState) { mutableStateListOf() } val selectedFiles = remember(selectedItemIds.size) { - mutableStateOf( - songsList.count { info -> - selectedItemIds.contains(info.id) - } - ) + mutableStateOf(songsList.count { info -> + selectedItemIds.contains(info.id) + }) } val selectedFileSizeSum by remember(selectedItemIds.size) { @@ -179,12 +177,9 @@ fun DownloadsHistoryPage( val checkBoxState by remember(selectedItemIds, visibleItemCount) { derivedStateOf { - if (selectedItemIds.isEmpty()) - ToggleableState.Off - else if (selectedItemIds.size == visibleItemCount.value && selectedItemIds.isNotEmpty()) - ToggleableState.On - else - ToggleableState.Indeterminate + if (selectedItemIds.isEmpty()) ToggleableState.Off + else if (selectedItemIds.size == visibleItemCount.value && selectedItemIds.isNotEmpty()) ToggleableState.On + else ToggleableState.Indeterminate } } @@ -192,95 +187,88 @@ fun DownloadsHistoryPage( isSelectEnabled = false } - Scaffold( - modifier = Modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - LargeTopAppBar( - title = { - Text( - modifier = Modifier, - text = stringResource(R.string.downloads_history) - ) - }, - navigationIcon = { - BackButton { - onBackPressed() - } - }, actions = { - Row(){ - IconToggleButton( - modifier = Modifier, - onCheckedChange = { isSelectEnabled = !isSelectEnabled }, - checked = isSelectEnabled, - enabled = songsList.isNotEmpty() - ) { - Icon( - Icons.Outlined.Checklist, - contentDescription = stringResource(R.string.multiselect_mode) - ) - } - } - }, scrollBehavior = scrollBehavior + Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { + LargeTopAppBar(title = { + Text( + modifier = Modifier, text = stringResource(R.string.downloads_history) ) - }, bottomBar = { - AnimatedVisibility( - isSelectEnabled, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut() - ) { - BottomAppBar( - modifier = Modifier + }, navigationIcon = { + BackButton { + onBackPressed() + } + }, actions = { + Row { + IconToggleButton( + modifier = Modifier, + onCheckedChange = { isSelectEnabled = !isSelectEnabled }, + checked = isSelectEnabled, + enabled = songsList.isNotEmpty() ) { - val selectAllText = stringResource(R.string.select_all) - TriStateCheckbox( - modifier = Modifier.semantics { - this.contentDescription = selectAllText - }, - state = checkBoxState, - onClick = { - when (checkBoxState) { - ToggleableState.On -> selectedItemIds.clear() - else -> { - for (item in songsList) { - if (!selectedItemIds.contains(item.id) - && item.filterSort(viewState) - ) { - selectedItemIds.add(item.id) - } + Icon( + Icons.Outlined.Checklist, + contentDescription = stringResource(R.string.multiselect_mode) + ) + } + } + }, scrollBehavior = scrollBehavior + ) + }, bottomBar = { + AnimatedVisibility( + isSelectEnabled, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + BottomAppBar( + modifier = Modifier + ) { + val selectAllText = stringResource(R.string.select_all) + TriStateCheckbox( + modifier = Modifier.semantics { + this.contentDescription = selectAllText + }, + state = checkBoxState, + onClick = { + when (checkBoxState) { + ToggleableState.On -> selectedItemIds.clear() + else -> { + for (item in songsList) { + if (!selectedItemIds.contains(item.id) && item.filterSort( + viewState + ) + ) { + selectedItemIds.add(item.id) } } } - }, - ) - Text( - modifier = Modifier.weight(1f), - text = stringResource(R.string.multiselect_item_count).format( - selectedFiles.value, - ), - style = MaterialTheme.typography.labelLarge + } + }, + ) + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.multiselect_item_count).format( + selectedFiles.value, + ), + style = MaterialTheme.typography.labelLarge + ) + IconButton( + onClick = { showRemoveMultipleItemsDialog = true }, + enabled = selectedItemIds.isNotEmpty() + ) { + Icon( + imageVector = Icons.Outlined.DeleteSweep, + contentDescription = stringResource(id = R.string.remove) ) - IconButton( - onClick = { showRemoveMultipleItemsDialog = true }, - enabled = selectedItemIds.isNotEmpty() - ) { - Icon( - imageVector = Icons.Outlined.DeleteSweep, - contentDescription = stringResource(id = R.string.remove) - ) - } } } } - ) { innerPaddings -> + }) { innerPaddings -> val cellCount = when (LocalWindowWidthState.current) { WindowWidthSizeClass.Expanded -> 2 else -> 1 } val span: (LazyGridItemSpanScope) -> GridItemSpan = { GridItemSpan(cellCount) } LazyVerticalGrid( - modifier = Modifier - .padding(innerPaddings), columns = GridCells.Fixed(cellCount) + modifier = Modifier.padding(innerPaddings), columns = GridCells.Fixed(cellCount) ) { if (filterSet.size > 1) { item { @@ -300,7 +288,8 @@ fun DownloadsHistoryPage( if (songsList.isEmpty()) { item { - Column(modifier = Modifier.fillMaxSize(), + Column( + modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { EmptyState( @@ -314,17 +303,14 @@ fun DownloadsHistoryPage( } for (song in songsList) { - item( - key = song.id, - contentType = { song.songPath.contains(AUDIO_REGEX) }) { + item(key = song.id, contentType = { song.songPath.contains(AUDIO_REGEX) }) { with(song) { AnimatedVisibility( visible = song.filterSort(viewState), exit = shrinkVertically() + fadeOut(), enter = expandVertically() + fadeIn() ) { - HistoryMediaItem( - modifier = Modifier, + HistoryMediaItem(modifier = Modifier, songName = songName, author = songAuthor, artworkUrl = thumbnailUrl, @@ -340,8 +326,12 @@ fun DownloadsHistoryPage( if (selectedItemIds.contains(id)) selectedItemIds.remove(id) else selectedItemIds.add(id) }, - onClick = { FilesUtil.openFile(songPath) } - ) { downloadsHistoryViewModel.showDrawer(scope, song) } + onClick = { FilesUtil.openFile(songPath) }) { + downloadsHistoryViewModel.showDrawer( + scope, + song + ) + } } } } @@ -351,16 +341,15 @@ fun DownloadsHistoryPage( DownloadHistoryBottomDrawer() if (showRemoveMultipleItemsDialog) { var deleteFile by remember { mutableStateOf(false) } - SpowloDialog( - onDismissRequest = { showRemoveMultipleItemsDialog = false }, + SpowloDialog(onDismissRequest = { showRemoveMultipleItemsDialog = false }, icon = { Icon(Icons.Outlined.DeleteSweep, null) }, - title = { Text(stringResource(R.string.delete_info)) }, text = { + title = { Text(stringResource(R.string.delete_info)) }, + text = { Column { Text( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp) - , + .padding(horizontal = 24.dp), text = stringResource(R.string.delete_multiple_items_msg).format( selectedFiles.value ) @@ -371,7 +360,8 @@ fun DownloadsHistoryPage( checked = deleteFile ) { deleteFile = !deleteFile } } - }, confirmButton = { + }, + confirmButton = { ConfirmButton { scope.launch { DatabaseUtil.deleteInfoListByIdList(selectedItemIds, deleteFile) @@ -379,12 +369,12 @@ fun DownloadsHistoryPage( showRemoveMultipleItemsDialog = false isSelectEnabled = false } - }, dismissButton = { + }, + dismissButton = { DismissButton { showRemoveMultipleItemsDialog = false } - } - ) + }) } } @@ -398,17 +388,17 @@ fun EmptyState(modifier: Modifier, text: String) { verticalArrangement = Arrangement.Center ) { Icon( - imageVector = Icons.Outlined.DownloadForOffline, + imageVector = Icons.Filled.DownloadForOffline, contentDescription = null, tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(64.dp) + modifier = Modifier.size(72.dp) ) - Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider(modifier = Modifier.height(16.dp).padding(horizontal = 32.dp)) Text( text = text, style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.secondary, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold ) } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt index 3fd3ed04..6407227f 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/binders/SpotifyPageBinder.kt @@ -1,8 +1,11 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.adamratzman.spotify.models.Album import com.adamratzman.spotify.models.Artist import com.adamratzman.spotify.models.Playlist @@ -18,39 +21,48 @@ fun SpotifyPageBinder( data: Any, type: SpotifyDataType, modifier: Modifier = Modifier, - trackDownloadCallback : (String, String) -> Unit, + trackDownloadCallback: (String, String) -> Unit, ) { - Column(modifier = modifier) { + + LazyColumn(modifier = modifier.padding(top = 6.dp), verticalArrangement = Arrangement.Top) { + when (type) { SpotifyDataType.ALBUM -> { val album = data as? Album - album?.let { - AlbumPage(album, modifier) + item { + album?.let { + AlbumPage(album, modifier, trackDownloadCallback) + } } } SpotifyDataType.ARTIST -> { val artist = data as? Artist - artist?.let { - ArtistPage(artist, modifier) + item { + artist?.let { + ArtistPage(artist, modifier) + } } } SpotifyDataType.PLAYLIST -> { val playlist = data as? Playlist - playlist?.let { - PlaylistViewPage(playlist, modifier, trackDownloadCallback) + item { + playlist?.let { + PlaylistViewPage(playlist, modifier, trackDownloadCallback) + } } + } SpotifyDataType.TRACK -> { val track = data as? Track - track?.let { - TrackPage(track,modifier,trackDownloadCallback) + item { + track?.let { + TrackPage(track, modifier, trackDownloadCallback) + } } } } } -} - -//data-type to SpotifyDataType \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt index d960283f..4ae9f81e 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/AlbumPage.kt @@ -1,14 +1,157 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +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.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import com.adamratzman.spotify.models.Album -import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage +import com.bobbyesp.spowlo.R +import com.bobbyesp.spowlo.ui.common.AsyncImageImpl +import com.bobbyesp.spowlo.ui.components.HorizontalDivider +import com.bobbyesp.spowlo.ui.components.MarqueeText +import com.bobbyesp.spowlo.ui.components.songs.metadata_viewer.TrackComponent +import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.dataStringToString @Composable fun AlbumPage( data: Album, - modifier: Modifier + modifier: Modifier, + trackDownloadCallback: (String, String) -> Unit ) { - NotImplementedPage() + val localConfig = LocalConfiguration.current + + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Top + ) { + Box( + modifier = Modifier + .clip(MaterialTheme.shapes.extraSmall) + .fillMaxWidth() + .padding(bottom = 6.dp), + contentAlignment = Alignment.Center + ) { + //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height + val size = (localConfig.screenHeightDp / 3) + AsyncImageImpl( + modifier = Modifier + .size(size.dp) + .aspectRatio( + 1f, matchHeightConstraintsFirst = true + ) + .clip(MaterialTheme.shapes.small), + model = data.images[0].url, + contentDescription = stringResource(id = R.string.track_artwork), + contentScale = ContentScale.Crop, + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 8.dp) + ) { + SelectionContainer { + MarqueeText( + text = data.name, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineMedium + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = data.artists.joinToString(", ") { it.name }, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + SelectionContainer { + Text( + text = dataStringToString( + data = data.type, additional = data.releaseDate.year.toString(), + ), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(alpha = 0.8f) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + if(data.externalUrls.spotify != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + FilledTonalIconButton( + onClick = { + trackDownloadCallback(data.externalUrls.spotify!!, data.name) + }, + modifier = Modifier.size(48.dp), + ) { + Icon( + imageVector = Icons.Filled.Download, + contentDescription = "Download full playlist icon", + modifier = Modifier + .weight(1f) + .padding(14.dp) + ) + } + + } + } + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) + if(data.tracks.size > 0) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + data.tracks.items.forEach { track -> + val taskName = StringBuilder().append(track.name).append(" - ").append( + track?.artists?.joinToString(", ") { it.name }).toString() + TrackComponent( + contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + songName = track.name, + artists = track.artists.joinToString(", ") { it.name }, + spotifyUrl = track.externalUrls.spotify ?: "", + isExplicit = track.explicit, + onClick = { + trackDownloadCallback( + track.externalUrls.spotify!!, + taskName + ) + } + ) + } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt index 213ec3dc..8f7947a1 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/ArtistPage.kt @@ -3,7 +3,7 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.adamratzman.spotify.models.Artist -import com.bobbyesp.spowlo.ui.pages.common.NotImplementedPage +import com.bobbyesp.spowlo.ui.pages.commonPages.NotImplementedPage @Composable fun ArtistPage( diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt index fad6f3d8..2f189e28 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/PlaylistViewPage.kt @@ -46,13 +46,15 @@ fun PlaylistViewPage( val localConfig = LocalConfiguration.current Column( - modifier = modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Top ) { Box( modifier = Modifier .clip(MaterialTheme.shapes.extraSmall) .fillMaxWidth() - .padding(top = 16.dp, bottom = 6.dp), contentAlignment = Alignment.Center + .padding(bottom = 6.dp), + contentAlignment = Alignment.Center ) { //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height val size = (localConfig.screenHeightDp / 3) @@ -94,7 +96,8 @@ fun PlaylistViewPage( SelectionContainer { Text( text = dataStringToString( - data = data.type, additional = data.followers.total.toString() + " " + App.context.getString(R.string.followers) + data = data.type, + additional = data.followers.total.toString() + " " + App.context.getString(R.string.followers) .lowercase() ), style = MaterialTheme.typography.bodySmall, @@ -109,9 +112,11 @@ fun PlaylistViewPage( modifier = Modifier.alpha(alpha = 0.8f) ) } - if(data.externalUrls.spotify != null){ + if (data.externalUrls.spotify != null) { Row( - modifier = Modifier.fillMaxWidth().padding(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically ) { @@ -132,30 +137,47 @@ fun PlaylistViewPage( } } - HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) - Column( - modifier = Modifier.fillMaxWidth() - ) { - //for every track in the playlist, show the track name and the artist name - data.tracks.items.forEach { track -> - val actualTrack = track.track?.asTrack - val taskName = StringBuilder().append(actualTrack?.name).append(" - ").append(actualTrack?.artists?.joinToString(", ") { it.name }).toString() - TrackComponent( - contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), - songName = actualTrack?.name ?: App.context.getString(R.string.unknown), - artists = actualTrack?.artists?.joinToString(", ") { it.name } ?: "", - spotifyUrl = actualTrack?.externalUrls?.spotify ?: "", - isExplicit = actualTrack?.explicit ?: false, - isPlaylist = true, - imageUrl = actualTrack?.album?.images?.get(0)?.url ?: "", - onClick = { - if (actualTrack != null) { - trackDownloadCallback(actualTrack.externalUrls.spotify!!, taskName) + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) + if (data.tracks.size > 0) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + //for every track in the playlist, show the track name and the artist name + data.tracks.items.forEach { track -> + val actualTrack = track.track?.asTrack + val taskName = StringBuilder().append(actualTrack?.name).append(" - ") + .append(actualTrack?.artists?.joinToString(", ") { it.name }).toString() + TrackComponent( + contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + songName = actualTrack?.name ?: App.context.getString(R.string.unknown), + artists = actualTrack?.artists?.joinToString(", ") { it.name } ?: "", + spotifyUrl = actualTrack?.externalUrls?.spotify ?: "", + isExplicit = actualTrack?.explicit ?: false, + isPlaylist = true, + imageUrl = actualTrack?.album?.images?.getOrNull(0)?.url ?: "", + onClick = { + if (actualTrack != null) { + trackDownloadCallback( + actualTrack.externalUrls.spotify!!, + taskName + ) + } } - } - ) + ) + } } } } } -} \ No newline at end of file +} + +/*@ExperimentalSerializationApi +object PlaylistSaver : Saver { + override fun restore(value: String): Playlist? { + return Json.decodeFromString(value) + } + + override fun SaverScope.save(value: Playlist?): String { + return Json.encodeToString(value) + } +}*/ \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt index dca76eb7..5757e5c3 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/pages/TrackPage.kt @@ -1,6 +1,5 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.pages -import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -60,12 +59,18 @@ fun TrackPage( var audioFeatures by rememberSaveable(stateSaver = AudioFeaturesSaver) { mutableStateOf(null) } + var trackData by rememberSaveable(stateSaver = TrackSaver) { + mutableStateOf(data) + } LaunchedEffect(Unit) { if (audioFeatures == null) { val feats = SpotifyApiRequests.providesGetAudioFeatures(data.id) audioFeatures = feats } + if (trackData != data) { + trackData = data + } } Column( @@ -75,7 +80,7 @@ fun TrackPage( modifier = Modifier .clip(MaterialTheme.shapes.extraSmall) .fillMaxWidth() - .padding(top = 16.dp, bottom = 6.dp), contentAlignment = Alignment.Center + .padding(bottom = 6.dp), contentAlignment = Alignment.Center ) { //calculate the image size based on the screen size and the aspect ratio as 1:1 (square) based on the height val size = (localConfig.screenHeightDp / 3) @@ -86,7 +91,7 @@ fun TrackPage( 1f, matchHeightConstraintsFirst = true ) .clip(MaterialTheme.shapes.small), - model = data.album.images[0].url, + model = trackData.album.images[0].url, contentDescription = stringResource(id = R.string.track_artwork), contentScale = ContentScale.Crop, ) @@ -98,7 +103,7 @@ fun TrackPage( ) { SelectionContainer { MarqueeText( - text = data.name, + text = trackData.name, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineMedium ) @@ -106,7 +111,7 @@ fun TrackPage( Spacer(modifier = Modifier.height(6.dp)) SelectionContainer { Text( - text = data.artists.joinToString(", ") { it.name }, + text = trackData.artists.joinToString(", ") { it.name }, style = MaterialTheme.typography.bodyMedium.copy( fontWeight = FontWeight.Bold ), @@ -117,7 +122,7 @@ fun TrackPage( SelectionContainer { Text( text = dataStringToString( - data = data.type, additional = data.album.releaseDate?.year.toString() + data = trackData.type, additional = trackData.album.releaseDate?.year.toString() ), style = MaterialTheme.typography.bodySmall, modifier = Modifier.alpha(alpha = 0.8f) @@ -125,18 +130,18 @@ fun TrackPage( } } HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) + Column( modifier = Modifier.fillMaxWidth() ) { - val taskName = StringBuilder().append(data.name).append(" - ").append(data.artists.joinToString(", ") { it.name }).toString() - TrackComponent( - contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), - songName = data.name, - artists = data.artists.joinToString(", ") { it.name }, - spotifyUrl = data.externalUrls.spotify!!, - isExplicit = data.explicit, - onClick = { trackDownloadCallback(data.externalUrls.spotify!!, taskName) } - ) + val taskName = StringBuilder().append(trackData.name).append(" - ") + .append(trackData.artists.joinToString(", ") { it.name }).toString() + TrackComponent(contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + songName = trackData.name, + artists = trackData.artists.joinToString(", ") { it.name }, + spotifyUrl = trackData.externalUrls.spotify!!, + isExplicit = trackData.explicit, + onClick = { trackDownloadCallback(trackData.externalUrls.spotify!!, taskName) }) } Spacer(modifier = Modifier.padding(vertical = 8.dp)) Column(modifier = Modifier.fillMaxWidth()) { @@ -147,13 +152,13 @@ fun TrackPage( ) { ExtraInfoCard( headlineText = stringResource(id = R.string.track_popularity), - bodyText = data.popularity.toString(), + bodyText = trackData.popularity.toString(), modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(16.dp)) ExtraInfoCard( headlineText = stringResource(id = R.string.track_duration), - bodyText = GeneralTextUtils.convertDuration(data.durationMs.toDouble()), + bodyText = GeneralTextUtils.convertDuration(trackData.durationMs.toDouble()), modifier = Modifier.weight(1f) ) } @@ -190,4 +195,15 @@ object AudioFeaturesSaver : Saver { override fun SaverScope.save(value: AudioFeatures?): String { return Json.encodeToString(value) } +} + +@ExperimentalSerializationApi +object TrackSaver : Saver { + override fun restore(value: String): Track { + return Json.decodeFromString(value) + } + + override fun SaverScope.save(value: Track): String { + return Json.encodeToString(value) + } } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt index ef88d3a8..313b4ec2 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPage.kt @@ -1,11 +1,10 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -13,15 +12,15 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bobbyesp.spowlo.R import com.bobbyesp.spowlo.ui.components.BackButton -import com.bobbyesp.spowlo.ui.pages.common.LoadingPage +import com.bobbyesp.spowlo.ui.pages.commonPages.LoadingPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.SpotifyPageBinder import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType @@ -31,14 +30,13 @@ fun PlaylistPage( onBackPressed: () -> Unit, playlistPageViewModel: PlaylistPageViewModel = hiltViewModel(), id: String, - type : String, + type: String, ) { - val scope = rememberCoroutineScope() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val viewState by playlistPageViewModel.viewStateFlow.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { + LaunchedEffect(id) { playlistPageViewModel.loadData(id, typeOfSpotifyDataType(type)) } @@ -55,38 +53,29 @@ fun PlaylistPage( is PlaylistDataState.Loaded -> { Scaffold(modifier = Modifier .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar(title = { - Text( - "WORK IN PROGRESS", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.alpha(scrollBehavior.state.overlappedFraction) - ) - }, navigationIcon = { - BackButton { onBackPressed() } - }, actions = { - }, - scrollBehavior = scrollBehavior + .nestedScroll(scrollBehavior.nestedScrollConnection) + , topBar = { + TopAppBar(title = { + Text( + text = stringResource(id = R.string.metadata_viewer), + style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp) ) - }) { paddings -> - LazyColumn( - Modifier - .padding(paddings) - .fillMaxSize()) { - item{ - Box(Modifier.animateItemPlacement()) { - SpotifyPageBinder( - data = state.data, - type = typeOfSpotifyDataType(type), - modifier = Modifier, - trackDownloadCallback = { url, name -> - playlistPageViewModel.downloadTrack(url, name) - }, ) - } - } - } + }, navigationIcon = { + BackButton { onBackPressed() } + }, actions = {}, scrollBehavior = scrollBehavior + ) + }) { paddings -> + SpotifyPageBinder( + data = state.data, + type = typeOfSpotifyDataType(type), + modifier = Modifier + .fillMaxSize() + .padding(paddings), + trackDownloadCallback = { url, name -> + playlistPageViewModel.downloadTrack(url, name) + }, + ) + } } } @@ -97,9 +86,4 @@ sealed class PlaylistDataState { object Loading : PlaylistDataState() class Error(val error: Exception) : PlaylistDataState() class Loaded(val data: Any) : PlaylistDataState() -} - -class ToolbarOptions( - val big: Boolean = false, - val alwaysVisible: Boolean = false -) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt index 13153b5e..a4d3d422 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/metadata_viewer/playlists/PlaylistPageViewModel.kt @@ -1,5 +1,6 @@ package com.bobbyesp.spowlo.ui.pages.metadata_viewer.playlists +import android.util.Log import androidx.lifecycle.ViewModel import com.bobbyesp.spowlo.Downloader import com.bobbyesp.spowlo.features.spotify_api.data.remote.SpotifyApiRequests @@ -29,7 +30,8 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { when (type) { SpotifyDataType.TRACK -> { kotlin.runCatching { - SpotifyApiRequests.getTrackById(id) + Log.d("SpotifyApiRequests", "provideGetTrackById($id)") + SpotifyApiRequests.provideGetTrackById(id) }.onSuccess { data -> mutableViewStateFlow.update { it.copy( @@ -49,7 +51,7 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { SpotifyDataType.ALBUM -> { kotlin.runCatching { - SpotifyApiRequests.getAlbumById(id) + SpotifyApiRequests.providesGetAlbumById(id) }.onSuccess { data -> mutableViewStateFlow.update { it.copy( @@ -70,7 +72,8 @@ class PlaylistPageViewModel @Inject constructor() : ViewModel() { SpotifyDataType.PLAYLIST -> { kotlin.runCatching { - SpotifyApiRequests.getPlaylistById(id) + Log.d("SpotifyApiRequests", "provideGetPlaylistById($id)") + SpotifyApiRequests.provideGetPlaylistById(id) }.onSuccess { data -> mutableViewStateFlow.update { it.copy( diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt index 17447d25..8f8da68b 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/searcher/SearcherPage.kt @@ -62,7 +62,7 @@ import com.bobbyesp.spowlo.ui.components.songs.search_feat.SearchingSongComponen import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.IndicatorBehindScrollableTabRow import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.getString import com.bobbyesp.spowlo.ui.dialogs.bottomsheets.tabIndicatorOffset -import com.bobbyesp.spowlo.ui.pages.common.ErrorPage +import com.bobbyesp.spowlo.ui.pages.commonPages.ErrorPage import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfDataToString import com.bobbyesp.spowlo.ui.pages.metadata_viewer.binders.typeOfSpotifyDataType import com.bobbyesp.spowlo.ui.theme.harmonizeWithPrimary @@ -108,7 +108,7 @@ fun SearcherPageImpl( viewState: SearcherPageViewModel.ViewState, onValueChange: (String) -> Unit, onItemClick: (String, String) -> Unit, - reloadPageCallback : () -> Unit = {} + reloadPageCallback: () -> Unit = {} ) { Scaffold(modifier = Modifier.fillMaxSize()) { with(viewState) { @@ -187,7 +187,11 @@ fun SearcherPageImpl( is ViewSearchState.Success -> { val pagerState = rememberPagerState(initialPage = 0) - val pages = listOf(SearcherPages.TRACKS, SearcherPages.PLAYLISTS) + val pages = listOf( + SearcherPages.TRACKS, + SearcherPages.PLAYLISTS, + SearcherPages.ALBUMS + ) val scope = rememberCoroutineScope() IndicatorBehindScrollableTabRow( @@ -221,9 +225,11 @@ fun SearcherPageImpl( } } - HorizontalPager(pageCount = pages.size, state = pagerState, modifier = Modifier - .animateContentSize() - .fillMaxSize()) { + HorizontalPager( + pageCount = pages.size, state = pagerState, modifier = Modifier + .animateContentSize() + .fillMaxSize() + ) { when (pages[it]) { SearcherPages.TRACKS -> { LazyColumn(modifier = Modifier.fillMaxSize()) { @@ -253,8 +259,14 @@ fun SearcherPageImpl( artworkUrl = track.album.images[2].url, songName = track.name, artists = artists.joinToString(", "), - spotifyUrl = track.externalUrls.spotify ?: "", - onClick = { onItemClick(track.type, track.id) }, + spotifyUrl = track.externalUrls.spotify + ?: "", + onClick = { + onItemClick( + track.type, + track.id + ) + }, type = typeOfDataToString( type = typeOfSpotifyDataType( track.type @@ -270,6 +282,7 @@ fun SearcherPageImpl( } } } + SearcherPages.PLAYLISTS -> { LazyColumn(modifier = Modifier.fillMaxSize()) { viewState.viewState.data.let { data -> @@ -297,7 +310,8 @@ fun SearcherPageImpl( songName = playlist.name, artists = playlist.owner.displayName ?: stringResource(R.string.unknown), - spotifyUrl = playlist.externalUrls.spotify ?: "", + spotifyUrl = playlist.externalUrls.spotify + ?: "", onClick = { onItemClick( playlist.type, @@ -319,6 +333,56 @@ fun SearcherPageImpl( } } } + + SearcherPages.ALBUMS -> { + LazyColumn(modifier = Modifier.fillMaxSize()) { + viewState.viewState.data.let { data -> + item { + Text( + text = stringResource(R.string.showing_results).format( + data.playlists?.size + ), + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier + .padding(16.dp) + .alpha(0.7f), + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Start, + fontWeight = FontWeight.Bold + ) + + } + data.albums?.items?.forEachIndexed { index, album -> + item { + SearchingSongComponent( + artworkUrl = album.images[0].url, + songName = album.name, + artists = album.artists[0].name, + spotifyUrl = album.externalUrls.spotify + ?: "", + onClick = { + onItemClick( + album.type, + album.id + ) + }, + type = typeOfDataToString( + type = typeOfSpotifyDataType( + album.type + ) + ) + ) + HorizontalDivider( + modifier = Modifier.alpha(0.35f), + color = MaterialTheme.colorScheme.primary.harmonizeWithPrimary() + ) + } + } + } + } + } } } } @@ -382,4 +446,5 @@ fun QueryTextBox( object SearcherPages { val TRACKS = getString(R.string.tracks) val PLAYLISTS = getString(R.string.playlists) + val ALBUMS = getString(R.string.albums) } \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt index 2ab1c822..5dbbc6c6 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/utils/DownloaderUtil.kt @@ -374,7 +374,6 @@ object DownloaderUtil { private fun insertInfoIntoDownloadHistory( songInfo: Song, filePaths: List ) { - filePaths.forEach { filePath -> val fullString = StringBuilder() fullString.append(songInfo.name) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 681bc33d..c23b18e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -324,4 +324,6 @@ The spotDL update failed. SpotDL is updated. You are using the version %1$s See playlist + Metadata viewer + Albums \ No newline at end of file From 7fbcfd6f892d1279a94477b63797de20c4bfaebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Font=C3=A1n?= Date: Wed, 5 Apr 2023 00:59:11 +0200 Subject: [PATCH 41/42] bugfix: Service not working --- app/src/main/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8df137ed..95ba6492 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -92,6 +92,10 @@ android:value="true" /> + + Date: Wed, 5 Apr 2023 10:07:00 +0200 Subject: [PATCH 42/42] feat: Added notification request --- .../bobbyesp/spowlo/ui/pages/InitialEntry.kt | 5 ++- .../ui/pages/downloader/DownloaderPage.kt | 40 +++++++++++++++++++ .../pages/downloader/DownloaderViewModel.kt | 2 + 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt index c4e5a126..0ab68f83 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/InitialEntry.kt @@ -51,6 +51,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.dialog import androidx.navigation.navArgument import androidx.navigation.navDeepLink +import androidx.navigation.navOptions import androidx.navigation.navigation import com.bobbyesp.library.SpotDL import com.bobbyesp.spowlo.App @@ -452,7 +453,9 @@ fun InitialEntry( onBackPressed, downloaderViewModel, navController - ) { id -> navController.navigate(Route.PLAYLIST_PAGE + "/" + "playlist" + "/" + id) } + ) { id -> navController.navigate(Route.PLAYLIST_PAGE + "/" + "playlist" + "/" + id, navOptions = navOptions { + launchSingleTop = true + }) } // } } diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt index 653a27de..bf63d34c 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderPage.kt @@ -2,6 +2,7 @@ package com.bobbyesp.spowlo.ui.pages.downloader import android.Manifest import android.os.Build +import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutSlowInEasing @@ -91,6 +92,7 @@ import com.bobbyesp.spowlo.ui.pages.settings.about.LocalAsset import com.bobbyesp.spowlo.ui.theme.harmonizeWith import com.bobbyesp.spowlo.utils.CONFIGURE import com.bobbyesp.spowlo.utils.DEBUG +import com.bobbyesp.spowlo.utils.NOTIFICATION import com.bobbyesp.spowlo.utils.PreferencesUtil import com.bobbyesp.spowlo.utils.PreferencesUtil.getBoolean import com.bobbyesp.spowlo.utils.ToastUtil @@ -124,6 +126,30 @@ fun DownloaderPage( } } + val notificationsPermission = rememberPermissionState( + permission = Manifest.permission.ACCESS_NOTIFICATION_POLICY + ) { b: Boolean -> + Log.d("DownloaderPage", "notificationsPermission: $b") + if (b) { + PreferencesUtil.updateValue(NOTIFICATION, true) + } else { + PreferencesUtil.updateValue(NOTIFICATION, false) + ToastUtil.makeToast(R.string.permission_denied) + } + } + + val modernNotificationPermission = rememberPermissionState( + permission = Manifest.permission.POST_NOTIFICATIONS + ) { b: Boolean -> + Log.d("DownloaderPage", "modernNotificationPermission: $b") + if (b) { + PreferencesUtil.updateValue(NOTIFICATION, true) + } else { + PreferencesUtil.updateValue(NOTIFICATION, false) + ToastUtil.makeToast(R.string.permission_denied) + } + } + //STATE FLOWS val viewState by downloaderViewModel.viewStateFlow.collectAsStateWithLifecycle() val downloaderState by Downloader.downloaderState.collectAsStateWithLifecycle() @@ -146,6 +172,20 @@ fun DownloaderPage( if (CONFIGURE.getBoolean()) navigateToDownloaderSheet() else checkPermissionOrDownload() keyboardController?.hide() + if(NOTIFICATION.getBoolean()){ + when(Build.VERSION.SDK_INT){ + in 23..31 -> { + if(notificationsPermission.status != PermissionStatus.Granted){ + notificationsPermission.launchPermissionRequest() + } + } + in 32..Int.MAX_VALUE -> { + if(modernNotificationPermission.status != PermissionStatus.Granted){ + modernNotificationPermission.launchPermissionRequest() + } + } + } + } } val songCardClicked = { diff --git a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderViewModel.kt index 747c3f70..83a249cc 100644 --- a/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderViewModel.kt +++ b/app/src/main/java/com/bobbyesp/spowlo/ui/pages/downloader/DownloaderViewModel.kt @@ -68,6 +68,7 @@ class DownloaderViewModel @Inject constructor() : ViewModel() { } fun startDownloadSong(skipInfoFetch: Boolean = false) { + val url = viewStateFlow.value.url Downloader.clearErrorState() if (!Downloader.isDownloaderAvailable()) @@ -76,6 +77,7 @@ class DownloaderViewModel @Inject constructor() : ViewModel() { showErrorMessage(R.string.url_empty) return } + //request notification permission Downloader.getInfoAndDownload(url, skipInfoFetch = skipInfoFetch) }