diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d1e8ec56..c18b214d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,8 +11,8 @@ plugins {
apply(plugin = "dagger.hilt.android.plugin")
val versionMajor = 0
-val versionMinor = 1
-val versionPatch = 3
+val versionMinor = 2
+val versionPatch = 0
val versionBuild = 0
val isStable = true
@@ -22,9 +22,11 @@ val navigationVersion: String by rootProject.extra
val roomVersion: String by rootProject.extra
val accompanistVersion: String by rootProject.extra
val composeMd3Version: String by rootProject.extra
+val youtubedlAndroidVersion: String by rootProject.extra
val coilVersion: String by rootProject.extra
val okhttpVersion: String by rootProject.extra
val hiltVersion: String by rootProject.extra
+val spotifyLibrary: String by rootProject.extra
val keystorePropertiesFile = rootProject.file("keystore.properties")
@@ -86,10 +88,43 @@ android {
)
if (keystorePropertiesFile.exists())
signingConfig = signingConfigs.getByName("debug")
+ buildConfigField("String",
+ "SPOTIFY_CLIENT_ID",
+ "\"abcad8ba647d4b0ebae797a8f444ac9b\"")
+ buildConfigField("String",
+ "SPOTIFY_REDIRECT_URI",
+ "\"spowlo://spotify-auth\"")
+ buildConfigField(
+ "String",
+ "SPOTIFY_REDIRECT_URI_PKCE",
+ "\"spowlo://spotify-pkce\""
+ )
+ packagingOptions {
+ resources.excludes.add("META-INF/*.kotlin_module")
+ }
+ matchingFallbacks.add(0, "debug")
+ matchingFallbacks.add(1, "release")
}
debug {
if (keystorePropertiesFile.exists())
signingConfig = signingConfigs.getByName("debug")
+ buildConfigField(
+ "String",
+ "SPOTIFY_CLIENT_ID",
+ "\"abcad8ba647d4b0ebae797a8f444ac9b\"")
+
+ buildConfigField(
+ "String",
+ "SPOTIFY_REDIRECT_URI_AUTH",
+ "\"spowlo://spotify-auth\"")
+
+ buildConfigField(
+ "String",
+ "SPOTIFY_REDIRECT_URI_PKCE",
+ "\"spowlo://spotify-pkce\""
+ )
+ matchingFallbacks.add(0, "debug")
+ matchingFallbacks.add(1, "release")
}
}
compileOptions {
@@ -165,11 +200,11 @@ dependencies {
kapt("androidx.hilt:hilt-compiler:1.0.0")
implementation("com.google.dagger:hilt-android:$hiltVersion")
kapt("com.google.dagger:hilt-android-compiler:$hiltVersion")
- //Room (Databases)
- /*implementation("androidx.room:room-runtime:$roomVersion")
+ //Room (Databases)
+ implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
- kapt("androidx.room:room-compiler:$roomVersion")*/
+ kapt("androidx.room:room-compiler:$roomVersion")
// Retrofit and okhttp
implementation("com.squareup.retrofit2:retrofit:2.9.0")
@@ -184,6 +219,11 @@ dependencies {
//SimpleStorage (SAF Simplifier)
implementation("com.anggrayudi:storage:1.5.0")
+ //Yt-dlp
+
+ //Spotify SDK Integration library
+ implementation("com.adamratzman:spotify-api-kotlin-core:$spotifyLibrary")
+
//Unit testing
testImplementation("junit:junit:4.13.2")
testImplementation("junit:junit:4.13.2")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a15cd5fa..1bcb69a9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,7 @@
android:name=".Spowlo"
android:allowBackup="true"
android:extractNativeLibs="true"
+ android:requestLegacyExternalStorage="true"
android:icon="@drawable/spowlo_icon"
android:label="@string/app_name"
android:roundIcon="@drawable/spowlo_icon"
@@ -64,6 +65,27 @@
android:name="autoStoreLocales"
android:value="true" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/Spowlo.kt b/app/src/main/java/com/bobbyesp/spowlo/Spowlo.kt
index 41893106..b6c58b2c 100644
--- a/app/src/main/java/com/bobbyesp/spowlo/Spowlo.kt
+++ b/app/src/main/java/com/bobbyesp/spowlo/Spowlo.kt
@@ -2,27 +2,66 @@ package com.bobbyesp.spowlo
import android.annotation.SuppressLint
import android.app.Application
+import android.content.ClipboardManager
import android.content.Context
-import com.anggrayudi.storage.SimpleStorageHelper
+import android.net.ConnectivityManager
+import com.bobbyesp.spowlo.data.auth.AuthModel
+import com.bobbyesp.spowlo.database.CommandTemplate
+import com.bobbyesp.spowlo.util.DatabaseUtil
+import com.bobbyesp.spowlo.util.PreferencesUtil
+import com.bobbyesp.spowlo.util.PreferencesUtil.AUDIO_DIRECTORY
+import com.bobbyesp.spowlo.util.PreferencesUtil.TEMPLATE_INDEX
import com.google.android.material.color.DynamicColors
import com.tencent.mmkv.MMKV
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
@HiltAndroidApp
class Spowlo : Application() {
+ lateinit var model: AuthModel
+
override fun onCreate() {
super.onCreate()
MMKV.initialize(this)
context = applicationContext
+ this.model = AuthModel
applicationScope = CoroutineScope(SupervisorJob())
DynamicColors.applyToActivitiesIfAvailable(this)
+
+ clipboard = getSystemService(ClipboardManager::class.java)
+ connectivityManager = getSystemService(ConnectivityManager::class.java)
+
+ applicationScope.launch((Dispatchers.IO)) {
+ if (!PreferencesUtil.containsKey(TEMPLATE_INDEX)) {
+ PreferencesUtil.updateInt(TEMPLATE_INDEX, 0)
+ DatabaseUtil.insertTemplate(
+ CommandTemplate(
+ 0,
+ context.getString(R.string.custom_command_template),
+ PreferencesUtil.getString(
+ PreferencesUtil.TEMPLATE, context.getString(R.string.template_example)
+ )
+ )
+ )
+ }
+ }
}
companion object{
private const val TAG = "Spowlo"
lateinit var applicationScope: CoroutineScope
+ lateinit var clipboard: ClipboardManager
+ lateinit var audioDownloadDir: String
+ var ytdlpVersion = ""
+ lateinit var connectivityManager: ConnectivityManager
+
+ fun updateDownloadDir(path: String) {
+ audioDownloadDir = path
+ PreferencesUtil.updateString(AUDIO_DIRECTORY, path)
+ }
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
diff --git a/app/src/main/java/com/bobbyesp/spowlo/data/auth/AuthModel.kt b/app/src/main/java/com/bobbyesp/spowlo/data/auth/AuthModel.kt
new file mode 100644
index 00000000..526a259e
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/data/auth/AuthModel.kt
@@ -0,0 +1,15 @@
+package com.bobbyesp.spowlo.data.auth
+
+import com.adamratzman.spotify.auth.SpotifyDefaultCredentialStore
+import com.bobbyesp.spowlo.BuildConfig
+import com.bobbyesp.spowlo.Spowlo
+
+object AuthModel {
+ val credentialStore by lazy {
+ SpotifyDefaultCredentialStore(
+ clientId = BuildConfig.SPOTIFY_CLIENT_ID,
+ redirectUri = BuildConfig.SPOTIFY_REDIRECT_URI_PKCE,
+ applicationContext = Spowlo.context
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/database/AppDatabase.kt b/app/src/main/java/com/bobbyesp/spowlo/database/AppDatabase.kt
new file mode 100644
index 00000000..d572c402
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/database/AppDatabase.kt
@@ -0,0 +1,12 @@
+package com.bobbyesp.spowlo.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+
+@Database(
+ entities = [DownloadedSongInfo::class, CommandTemplate::class], version = 3,
+ exportSchema = false
+)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun songInfoDao(): SongInfoDao
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/database/CommandTemplate.kt b/app/src/main/java/com/bobbyesp/spowlo/database/CommandTemplate.kt
new file mode 100644
index 00000000..175043e2
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/database/CommandTemplate.kt
@@ -0,0 +1,12 @@
+package com.bobbyesp.spowlo.database
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+@kotlinx.serialization.Serializable
+data class CommandTemplate(
+ @PrimaryKey(autoGenerate = true) val id: Int,
+ val name: String,
+ val template: String
+)
diff --git a/app/src/main/java/com/bobbyesp/spowlo/database/DownloadedSongInfo.kt b/app/src/main/java/com/bobbyesp/spowlo/database/DownloadedSongInfo.kt
new file mode 100644
index 00000000..90482e7c
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/database/DownloadedSongInfo.kt
@@ -0,0 +1,17 @@
+package com.bobbyesp.spowlo.database
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "DownloadedSongInfo")
+data class DownloadedSongInfo(
+ @PrimaryKey(autoGenerate = true) val id: Int,
+ val songTitle: String,
+ val songArtist: String,
+ val songUrl: String,
+ val thumbnailUrl: String,
+ val songPath: String,
+ @ColumnInfo(defaultValue = "Unknown")
+ val extractor: String = "Unknown"
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/database/SongInfoDao.kt b/app/src/main/java/com/bobbyesp/spowlo/database/SongInfoDao.kt
new file mode 100644
index 00000000..b0438437
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/database/SongInfoDao.kt
@@ -0,0 +1,43 @@
+package com.bobbyesp.spowlo.database
+
+import androidx.room.*
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface SongInfoDao {
+ @Insert
+ suspend fun insertAll(vararg info: DownloadedSongInfo)
+
+ @Query("select * from DownloadedSongInfo")
+ fun getAllMedia(): Flow>
+
+ @Query("select * from DownloadedSongInfo where id=:id")
+ suspend fun getInfoById(id: Int): DownloadedSongInfo
+
+ @Delete
+ suspend fun delete(info: DownloadedSongInfo)
+
+ @Query("DELETE FROM DownloadedSongInfo WHERE id = :id")
+ suspend fun deleteInfoById(id: Int)
+
+ @Query("DELETE FROM DownloadedSongInfo WHERE songPath = :path")
+ suspend fun deleteInfoByPath(path: String)
+
+ @Query("SELECT * FROM CommandTemplate")
+ fun getTemplateFlow(): Flow>
+
+ @Query("SELECT * FROM CommandTemplate")
+ suspend fun getTemplateList(): List
+
+ @Insert
+ suspend fun insertTemplate(template: CommandTemplate)
+
+ @Update
+ suspend fun updateTemplate(template: CommandTemplate)
+
+ @Delete
+ suspend fun deleteTemplate(template: CommandTemplate)
+
+ @Query("SELECT * FROM CommandTemplate where id = :id")
+ suspend fun getTemplateById(id: Int): CommandTemplate
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/domain/spotify/web_api/auth/SpotifyPkceLoginActivityImpl.kt b/app/src/main/java/com/bobbyesp/spowlo/domain/spotify/web_api/auth/SpotifyPkceLoginActivityImpl.kt
new file mode 100644
index 00000000..bdaf695c
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/domain/spotify/web_api/auth/SpotifyPkceLoginActivityImpl.kt
@@ -0,0 +1,31 @@
+package com.bobbyesp.spowlo.domain.spotify.web_api.auth
+
+import android.app.Activity
+import com.adamratzman.spotify.SpotifyClientApi
+import com.adamratzman.spotify.SpotifyScope
+import com.adamratzman.spotify.auth.pkce.AbstractSpotifyPkceLoginActivity
+import com.bobbyesp.spowlo.BuildConfig
+import com.bobbyesp.spowlo.Spowlo
+import com.bobbyesp.spowlo.presentation.MainActivity
+import com.bobbyesp.spowlo.util.Utils.makeToast
+
+internal var pkceClassBackTo: Class? = null
+
+class SpotifyPkceLoginActivityImpl: AbstractSpotifyPkceLoginActivity() {
+ override val clientId = BuildConfig.SPOTIFY_CLIENT_ID
+ override val redirectUri = BuildConfig.SPOTIFY_REDIRECT_URI_PKCE
+ override val scopes = SpotifyScope.values().toList()
+
+ override fun onFailure(exception: Exception) {
+ exception.printStackTrace()
+ pkceClassBackTo = null
+ makeToast("Auth failed: ${exception.message}")
+ }
+
+ override fun onSuccess(api: SpotifyClientApi) {
+ val model = (application as Spowlo).model
+ model.credentialStore.setSpotifyApi(api)
+ val classBackTo = pkceClassBackTo ?: MainActivity::class.java
+ pkceClassBackTo = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/domain/spotify/web_api/utilities/VerifyLoggedInUtils.kt b/app/src/main/java/com/bobbyesp/spowlo/domain/spotify/web_api/utilities/VerifyLoggedInUtils.kt
new file mode 100644
index 00000000..f4b37c3c
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/domain/spotify/web_api/utilities/VerifyLoggedInUtils.kt
@@ -0,0 +1,51 @@
+package com.bobbyesp.spowlo.domain.spotify.web_api.utilities
+
+import android.app.Activity
+import com.adamratzman.spotify.SpotifyClientApi
+import com.adamratzman.spotify.SpotifyException
+import com.adamratzman.spotify.auth.pkce.startSpotifyClientPkceLoginActivity
+import com.bobbyesp.spowlo.data.auth.AuthModel
+import com.bobbyesp.spowlo.domain.spotify.web_api.auth.SpotifyPkceLoginActivityImpl
+import com.bobbyesp.spowlo.domain.spotify.web_api.auth.pkceClassBackTo
+import kotlinx.coroutines.runBlocking
+
+fun Activity.guardValidSpotifyApi(
+ classBackTo: Class? = null,
+ alreadyTriedToReauthenticate: Boolean = false,
+ block: suspend (api: SpotifyClientApi) -> T
+): T? {
+ return runBlocking {
+ try {
+ val api = AuthModel.credentialStore.getSpotifyClientPkceApi() ?: throw SpotifyException.ReAuthenticationNeededException()
+ block(api)
+ } catch (e: SpotifyException) {
+ e.printStackTrace()
+ val api = AuthModel.credentialStore.getSpotifyClientPkceApi()!!
+ if (!alreadyTriedToReauthenticate) {
+ try {
+ api.refreshToken()
+ AuthModel.credentialStore.spotifyToken = api.token
+ block(api)
+ } catch (e: SpotifyException.ReAuthenticationNeededException) {
+ e.printStackTrace()
+ return@runBlocking guardValidSpotifyApi(
+ classBackTo = classBackTo,
+ alreadyTriedToReauthenticate = true,
+ block = block
+ )
+ } catch (e: IllegalArgumentException) {
+ e.printStackTrace()
+ return@runBlocking guardValidSpotifyApi(
+ classBackTo = classBackTo,
+ alreadyTriedToReauthenticate = true,
+ block = block
+ )
+ }
+ } else {
+ pkceClassBackTo = classBackTo
+ startSpotifyClientPkceLoginActivity(SpotifyPkceLoginActivityImpl::class.java)
+ null
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/MainActivity.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/MainActivity.kt
index e7c41c7d..b6bca9c2 100644
--- a/app/src/main/java/com/bobbyesp/spowlo/presentation/MainActivity.kt
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/MainActivity.kt
@@ -1,5 +1,6 @@
package com.bobbyesp.spowlo.presentation
+import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.util.Log
@@ -7,21 +8,36 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Download
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.core.os.LocaleListCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
+import com.bobbyesp.spowlo.R
import com.bobbyesp.spowlo.Spowlo.Companion.applicationScope
import com.bobbyesp.spowlo.Spowlo.Companion.context
-import com.bobbyesp.spowlo.presentation.ui.common.LocalDarkTheme
-import com.bobbyesp.spowlo.presentation.ui.common.LocalDynamicColorSwitch
-import com.bobbyesp.spowlo.presentation.ui.common.LocalSeedColor
-import com.bobbyesp.spowlo.presentation.ui.common.SettingsProvider
+import com.bobbyesp.spowlo.presentation.ui.common.*
+import com.bobbyesp.spowlo.presentation.ui.components.bottomNavBar.BottomNavBar
+import com.bobbyesp.spowlo.presentation.ui.components.bottomNavBar.NavBarItem
import com.bobbyesp.spowlo.presentation.ui.pages.InitialEntry
+import com.bobbyesp.spowlo.presentation.ui.pages.downloader_page.SearcherViewModel
import com.bobbyesp.spowlo.presentation.ui.pages.home.HomeViewModel
import com.bobbyesp.spowlo.presentation.ui.theme.SpowloTheme
import com.bobbyesp.spowlo.util.PreferencesUtil
+import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -29,10 +45,13 @@ import kotlinx.coroutines.runBlocking
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
-
private val homeViewModel: HomeViewModel by viewModels()
+ private val searcherViewModel: SearcherViewModel by viewModels()
- @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
+ @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
+ @OptIn(ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalMaterial3Api::class,
+ ExperimentalAnimationApi::class
+ )
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -40,28 +59,75 @@ class MainActivity : ComponentActivity() {
v.setPadding(0, 0, 0, 0)
insets
}
- runBlocking {
+ context = this.baseContext
+ runBlocking {
if (Build.VERSION.SDK_INT < 33)
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(PreferencesUtil.getLanguageConfiguration())
)
}
- context = this.baseContext
- setContent {
- val windowSizeClass = calculateWindowSizeClass(this)
- SettingsProvider(windowSizeClass.widthSizeClass){
- SpowloTheme(
- darkTheme = LocalDarkTheme.current.isDarkTheme(),
- isHighContrastModeEnabled = LocalDarkTheme.current.isHighContrastModeEnabled,
- seedColor = LocalSeedColor.current,
- isDynamicColorEnabled = LocalDynamicColorSwitch.current,
- ) {
- InitialEntry(homeViewModel)
+ setContent {
+ val navController = rememberAnimatedNavController()
+ val windowSizeClass = calculateWindowSizeClass(this)
+ //if the current route is not in the list of routes, then hide the nav bar modifying the visible var
+ val visible = remember { mutableStateOf(true) }
+
+ /*if current route is not home or settings, change the visible var to false
+ * INFO: Hide the navbar when the user is in a page that is not the ones that are in the navbar
+ */
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ visible.value =
+ destination.route in listOf(Route.HOME, /*Route.SETTINGS,*/ Route.SEARCHER_PAGE)
+ }
+ SettingsProvider(windowSizeClass.widthSizeClass) {
+ SpowloTheme(
+ darkTheme = LocalDarkTheme.current.isDarkTheme(),
+ isHighContrastModeEnabled = LocalDarkTheme.current.isHighContrastModeEnabled,
+ seedColor = LocalSeedColor.current,
+ isDynamicColorEnabled = LocalDynamicColorSwitch.current,
+ ) {
+ Scaffold(
+ bottomBar = {
+ BottomNavBar(
+ items = listOf(
+ NavBarItem(
+ name = stringResource(id = R.string.home),
+ icon = Icons.Filled.Home,
+ route = Route.HOME
+ ),
+ /*NavBarItem(
+ name = stringResource(id = R.string.settings),
+ icon = Icons.Filled.Settings,
+ route = Route.SETTINGS,
+ ),*/
+ NavBarItem(
+ name = stringResource(id = R.string.searcher),
+ icon = Icons.Filled.Search,
+ route = Route.SEARCHER_PAGE,
+ ),
+ ), navController = navController,
+ onItemClicked = {
+ //if the current route is the same as the one we are trying to navigate to, do nothing
+ if (navController.currentDestination?.route != it.route) {
+ navController.navigate(it.route)
+ }
+ },
+ visible = visible.value
+ )
+ }) {
+ //If the user is at a route different from home or settings, hide the bottom nav bar
+ InitialEntry(
+ homeViewModel,
+ modifier = Modifier.padding(paddingValues = it),
+ navController = navController,
+ searcherViewModel = searcherViewModel,
+ activity = this@MainActivity
+ )
+ }
+ }
}
}
-
}
- }
companion object {
private const val TAG = "MainActivity"
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/common/Route.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/common/Route.kt
index c2a95c87..d99fe165 100644
--- a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/common/Route.kt
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/common/Route.kt
@@ -11,5 +11,9 @@ class Route {
const val DISPLAY_SETTINGS = "display_settings"
const val ABOUT_SETTINGS = "about_settings"
const val DARK_THEME_SELECTOR = "dark_theme_selector"
+ const val DOWNLOADER = "downloader"
+ const val SEARCHER_PAGE = "searcher_page"
+ const val TEMPLATE = "template"
+ const val WELCOME_PAGE = "welcome_page"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/common/SettingsProvider.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/common/SettingsProvider.kt
index 08ccee42..b815efce 100644
--- a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/common/SettingsProvider.kt
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/common/SettingsProvider.kt
@@ -2,11 +2,17 @@ package com.bobbyesp.spowlo.presentation.ui.common
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.*
+import androidx.compose.ui.platform.LocalContext
+import coil.ImageLoader
+import com.bobbyesp.spowlo.Spowlo.Companion.context
import com.bobbyesp.spowlo.presentation.ui.theme.ColorScheme.DEFAULT_SEED_COLOR
import com.bobbyesp.spowlo.util.PreferencesUtil
val LocalDarkTheme = compositionLocalOf { PreferencesUtil.DarkThemePreference() }
val LocalSeedColor = compositionLocalOf { DEFAULT_SEED_COLOR }
+val LocalVideoThumbnailLoader = staticCompositionLocalOf {
+ ImageLoader.Builder(context).build()
+}
val LocalWindowWidthState = staticCompositionLocalOf { WindowWidthSizeClass.Compact }
val settingFlow = PreferencesUtil.AppSettingsStateFlow
val LocalDynamicColorSwitch = compositionLocalOf { false }
@@ -16,6 +22,8 @@ fun SettingsProvider(windowWidthSizeClass: WindowWidthSizeClass, content: @Compo
val appSettingsState = settingFlow.collectAsState().value
CompositionLocalProvider(
LocalDarkTheme provides appSettingsState.darkTheme,
+ LocalVideoThumbnailLoader provides ImageLoader.Builder(LocalContext.current)
+ .build(),
LocalSeedColor provides appSettingsState.seedColor,
LocalWindowWidthState provides windowWidthSizeClass,
LocalDynamicColorSwitch provides appSettingsState.isDynamicColorEnabled,
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/RelevantInfoItem.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/RelevantInfoItem.kt
index 5e3eb67f..e10af26b 100644
--- a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/RelevantInfoItem.kt
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/RelevantInfoItem.kt
@@ -48,6 +48,7 @@ fun RelevantInfoItem(
)
}
}
+
Divider()
Row(
@@ -62,8 +63,10 @@ fun RelevantInfoItem(
)
Box(modifier = Modifier
.fillMaxWidth()
- .wrapContentSize(Alignment.CenterEnd)){
- Row(modifier = Modifier) {
+ .wrapContentSize(Alignment.CenterEnd),
+ contentAlignment = Alignment.Center)
+ {
+ Row(modifier = Modifier.padding(start = 4.dp, end = 4.dp)) {
Text(
text = originalSpotifyVersion,
style = MaterialTheme.typography.bodyMedium,
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/bottomNavBar/BottomAppBar.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/bottomNavBar/BottomAppBar.kt
new file mode 100644
index 00000000..3d560664
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/bottomNavBar/BottomAppBar.kt
@@ -0,0 +1,56 @@
+package com.bobbyesp.spowlo.presentation.ui.components.bottomNavBar
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.*
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavController
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.currentBackStackEntryAsState
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun BottomNavBar(
+ modifier: Modifier = Modifier,
+ items: List,
+ navController: NavController,
+ onItemClicked: (NavBarItem) -> Unit,
+ visible: Boolean = true
+) {
+ val backStackEntry = navController.currentBackStackEntryAsState()
+ AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) {
+ NavigationBar(
+ modifier = modifier,
+ tonalElevation = 3.dp
+ ){
+ items.forEach { item ->
+ val selected = item.route == backStackEntry.value?.destination?.route
+ NavigationBarItem(
+ icon = {
+ Column(horizontalAlignment = CenterHorizontally) {
+ if(item.badgeCount > 0){
+ BadgedBox(badge = {
+ Text(text = item.badgeCount.toString())
+ }) {
+ Icon(imageVector = item.icon, contentDescription = item.name)
+ }
+ }
+ else {
+ Icon(imageVector = item.icon, contentDescription = item.name)
+ }
+ }
+ },
+ label = {
+ Text(text = item.name) },
+ selected = selected,
+ onClick = { onItemClicked(item) }
+ )
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/bottomNavBar/NavBarItem.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/bottomNavBar/NavBarItem.kt
new file mode 100644
index 00000000..95938c5c
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/bottomNavBar/NavBarItem.kt
@@ -0,0 +1,10 @@
+package com.bobbyesp.spowlo.presentation.ui.components.bottomNavBar
+
+import androidx.compose.ui.graphics.vector.ImageVector
+
+data class NavBarItem(
+ val name: String,
+ val icon: ImageVector,
+ val route: String,
+ val badgeCount: Int = 0
+)
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/songs/TrackItem.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/songs/TrackItem.kt
new file mode 100644
index 00000000..ae9d753c
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/components/songs/TrackItem.kt
@@ -0,0 +1,63 @@
+package com.bobbyesp.spowlo.presentation.ui.components.songs
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+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.clip
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
+import com.adamratzman.spotify.models.Track
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TrackItem(
+ modifier: Modifier = Modifier,
+ track: Track,
+ onClick: (Track) -> Unit
+) {
+ Surface(
+ modifier = modifier.clickable(onClick = { onClick(track) })
+ ) {
+ Row(
+ modifier = Modifier.padding(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .size(100.dp)
+ .padding(start = 10.dp, end = 15.dp, top = 5.dp, bottom = 5.dp)
+ .clip(MaterialTheme.shapes.medium),
+ contentScale = ContentScale.Fit,
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(track.album.images.firstOrNull()?.url)
+ .crossfade(true)
+ .build(),
+ contentDescription = "Album cover"
+ )
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Text(
+ text = track.name,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.bodyLarge
+ )
+ Text(
+ text = track.artists.joinToString(", ") { it.name },
+ style = MaterialTheme.typography.bodySmall
+ )
+
+ }
+ }
+
+ }
+}
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/InitialEntry.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/InitialEntry.kt
index 0535a422..d8cf3979 100644
--- a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/InitialEntry.kt
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/InitialEntry.kt
@@ -12,41 +12,48 @@ 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.material.MaterialTheme
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
-import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavHostController
import com.bobbyesp.spowlo.presentation.ui.common.LocalWindowWidthState
import com.bobbyesp.spowlo.presentation.ui.common.Route
import com.bobbyesp.spowlo.presentation.ui.common.animatedComposable
import com.bobbyesp.spowlo.presentation.ui.components.UpdateDialog
import com.bobbyesp.spowlo.presentation.ui.pages.home.HomePage
import com.bobbyesp.spowlo.presentation.ui.pages.home.HomeViewModel
-import com.bobbyesp.spowlo.presentation.ui.pages.placeholders.PagePlaceholder
import com.bobbyesp.spowlo.presentation.ui.pages.settings.SettingsPage
import com.bobbyesp.spowlo.presentation.ui.pages.settings.appearence.AppearancePreferences
import com.bobbyesp.spowlo.presentation.ui.pages.settings.appearence.DarkThemePreferences
import com.bobbyesp.spowlo.presentation.ui.pages.settings.appearence.LanguagesPreferences
-import com.bobbyesp.spowlo.util.PreferencesUtil
import com.bobbyesp.spowlo.util.UpdateUtil
import com.bobbyesp.spowlo.util.Utils
import com.google.accompanist.navigation.animation.AnimatedNavHost
-import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import com.bobbyesp.spowlo.R
+import com.bobbyesp.spowlo.presentation.ui.pages.downloader_page.SearcherPage
+import com.bobbyesp.spowlo.presentation.ui.pages.downloader_page.SearcherViewModel
+import com.bobbyesp.spowlo.presentation.ui.pages.welcome_page.WelcomePage
+import com.bobbyesp.spowlo.util.PreferencesUtil
+import com.bobbyesp.spowlo.util.PreferencesUtil.IS_LOGGED
private const val TAG = "InitialEntry"
-@OptIn(ExperimentalAnimationApi::class)
+@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
@Composable
-fun InitialEntry(homeViewModel: HomeViewModel) {
- val navController = rememberAnimatedNavController()
+fun InitialEntry(homeViewModel: HomeViewModel,
+ modifier: Modifier = Modifier,
+ navController: NavHostController,
+ searcherViewModel: SearcherViewModel,
+ activity: androidx.activity.ComponentActivity? = null)
+{
+
val context = LocalContext.current
val scope = rememberCoroutineScope()
var updateJob: Job? = null
@@ -54,6 +61,7 @@ fun InitialEntry(homeViewModel: HomeViewModel) {
var showUpdateDialog by rememberSaveable { mutableStateOf(false) }
var currentDownloadStatus by remember { mutableStateOf(UpdateUtil.DownloadStatus.NotYet as UpdateUtil.DownloadStatus) }
var latestRelease by remember { mutableStateOf(UpdateUtil.LatestRelease()) }
+ val searcherPageLoaded by remember { mutableStateOf(searcherViewModel.stateFlow.value.loaded) }
val settings =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
UpdateUtil.installLatestApk()
@@ -77,57 +85,75 @@ fun InitialEntry(homeViewModel: HomeViewModel) {
}
}
}
- val viewState = homeViewModel.stateFlow.collectAsState()
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(androidx.compose.material3.MaterialTheme.colorScheme.background)
- ){
- AnimatedNavHost(
+
+ val homeviewState = homeViewModel.stateFlow.collectAsState()
+ val searcherViewState = searcherViewModel.stateFlow.collectAsState()
+
+
+ Box(modifier = modifier){
+ Box(
modifier = Modifier
- .fillMaxWidth(
- when (LocalWindowWidthState.current) {
- WindowWidthSizeClass.Compact -> 1f
- WindowWidthSizeClass.Expanded -> 0.5f
- else -> 0.8f
+ .fillMaxSize()
+ .background(androidx.compose.material3.MaterialTheme.colorScheme.background)
+ ){
+ AnimatedNavHost(
+ modifier = Modifier
+ .fillMaxWidth(
+ when (LocalWindowWidthState.current) {
+ WindowWidthSizeClass.Compact -> 1f
+ WindowWidthSizeClass.Expanded -> 0.5f
+ else -> 0.8f
+ }
+ )
+ .align(Alignment.Center),
+ navController = navController,
+ startDestination = routeIfLogged()) {
+
+ animatedComposable(Route.HOME){
+ HomePage(navController = navController, homeViewModel = homeViewModel)
+ if (!homeviewState.value.loaded){
+ homeViewModel.setup()
}
- )
- .align(Alignment.Center),
- navController = navController,
- startDestination = Route.HOME) {
-
- animatedComposable(Route.HOME){
- HomePage(navController = navController, homeViewModel = homeViewModel)
- if (!viewState.value.loaded){
- homeViewModel.setup()
}
- }
- animatedComposable(Route.SETTINGS){
- SettingsPage(navController)
- }
+ animatedComposable(Route.SETTINGS){
+ SettingsPage(navController)
+ }
- animatedComposable(Route.ABOUT){
- }
+ animatedComposable(Route.ABOUT){
+ }
- animatedComposable(Route.DISPLAY_SETTINGS){
- AppearancePreferences(navController)
- }
+ animatedComposable(Route.DISPLAY_SETTINGS){
+ AppearancePreferences(navController)
+ }
- animatedComposable(Route.LANGUAGES){
- LanguagesPreferences{
- onBackPressed()
+ animatedComposable(Route.LANGUAGES){
+ LanguagesPreferences{
+ onBackPressed()
+ }
}
- }
- animatedComposable(Route.DARK_THEME_SELECTOR){
- DarkThemePreferences {
- onBackPressed()
+ animatedComposable(Route.DARK_THEME_SELECTOR){
+ DarkThemePreferences {
+ onBackPressed()
+ }
}
- }
+ animatedComposable(Route.WELCOME_PAGE){
+ WelcomePage(navController = navController, activity = activity)
+ }
+ animatedComposable(Route.SEARCHER_PAGE){
+ SearcherPage(navController = navController,
+ searcherViewModel = searcherViewModel,
+ activity = activity)
+ if(!searcherPageLoaded){
+ searcherViewModel.setup()
+ }
+ }
+ }
}
}
+
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
kotlin.runCatching {
@@ -171,4 +197,12 @@ fun InitialEntry(homeViewModel: HomeViewModel) {
downloadStatus = currentDownloadStatus
)
}
+}
+
+fun routeIfLogged(): String{
+ return if (PreferencesUtil.getValue(IS_LOGGED)){
+ Route.HOME
+ } else {
+ Route.WELCOME_PAGE
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/downloader_page/SearcherPage.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/downloader_page/SearcherPage.kt
new file mode 100644
index 00000000..caea087b
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/downloader_page/SearcherPage.kt
@@ -0,0 +1,164 @@
+package com.bobbyesp.spowlo.presentation.ui.pages.downloader_page
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Clear
+import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
+import com.bobbyesp.spowlo.R
+import com.bobbyesp.spowlo.Spowlo.Companion.context
+
+import com.bobbyesp.spowlo.data.auth.AuthModel
+import com.bobbyesp.spowlo.presentation.ui.components.songs.TrackItem
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+
+@OptIn(
+ ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class,
+ ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class
+)
+@Composable
+fun SearcherPage(
+ navController: NavController,
+ searcherViewModel: SearcherViewModel = hiltViewModel(),
+ activity: Activity? = null
+) {
+ lateinit var model: AuthModel
+ val viewState = searcherViewModel.stateFlow.collectAsState()
+
+ with(viewState.value) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background),
+ color = MaterialTheme.colorScheme.background,
+ ) {
+ if (!logged) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ Button(onClick = {
+ searcherViewModel.spotifyPkceLogin(activity)
+ }) {
+ Text("Connect to Spotify (spotify-web-api-kotlin integration, PKCE auth)")
+ }
+ }
+ } else {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ verticalArrangement = Arrangement.Top
+ ) {
+ SearchSongTextBox(
+ songName = searcherViewModel.searchQuery.value,
+ onValueChange = searcherViewModel::onSearch,
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ LazyColumn {
+ items(
+ items = listOfTracks, itemContent = { track ->
+ TrackItem(track = track, onClick = {
+ val browserIntent =
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(track.externalUrls.first { it.name == "spotify" }.url)
+ ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ ContextCompat.startActivity(
+ context,
+ browserIntent,
+ null
+ )
+ })
+ Divider()
+ })
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SearchSongTextBox(
+ songName: String,
+ onValueChange: (String) -> Unit
+) {
+ OutlinedTextField(
+ value = songName,
+ onValueChange = onValueChange,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(4.dp),
+ placeholder = {
+ Text(stringResource(id = R.string.search_song))
+ },
+ leadingIcon = {
+ Icon(imageVector = Icons.Rounded.Search, contentDescription = null)
+ },
+ trailingIcon = {
+ if (songName.isNotEmpty()) {
+ IconButton(onClick = {
+ onValueChange("")
+ }) {
+ Icon(imageVector = Icons.Rounded.Clear, contentDescription = null)
+ }
+ }
+ },
+ singleLine = true,
+ colors = TextFieldDefaults.outlinedTextFieldColors(
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
+ unfocusedBorderColor = MaterialTheme.colorScheme.primary,
+ focusedLabelColor = MaterialTheme.colorScheme.primary,
+ unfocusedLabelColor = MaterialTheme.colorScheme.primary,
+ cursorColor = MaterialTheme.colorScheme.primary,
+ leadingIconColor = MaterialTheme.colorScheme.primary,
+ trailingIconColor = MaterialTheme.colorScheme.primary,
+ textColor = MaterialTheme.colorScheme.primary,
+ disabledLabelColor = MaterialTheme.colorScheme.primary,
+ disabledBorderColor = MaterialTheme.colorScheme.primary,
+ disabledLeadingIconColor = MaterialTheme.colorScheme.primary,
+ disabledTrailingIconColor = MaterialTheme.colorScheme.primary,
+ errorBorderColor = MaterialTheme.colorScheme.primary,
+ errorLabelColor = MaterialTheme.colorScheme.primary,
+ errorCursorColor = MaterialTheme.colorScheme.primary,
+ errorLeadingIconColor = MaterialTheme.colorScheme.primary,
+ errorTrailingIconColor = MaterialTheme.colorScheme.primary,
+ backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/downloader_page/SearcherViewModel.kt b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/downloader_page/SearcherViewModel.kt
new file mode 100644
index 00000000..6aaef123
--- /dev/null
+++ b/app/src/main/java/com/bobbyesp/spowlo/presentation/ui/pages/downloader_page/SearcherViewModel.kt
@@ -0,0 +1,136 @@
+package com.bobbyesp.spowlo.presentation.ui.pages.downloader_page
+
+import android.app.Activity
+import android.util.Log
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.adamratzman.spotify.auth.implicit.startSpotifyImplicitLoginActivity
+import com.adamratzman.spotify.auth.pkce.startSpotifyClientPkceLoginActivity
+import com.adamratzman.spotify.models.Track
+import com.adamratzman.spotify.notifications.SpotifyBroadcastEventData
+import com.bobbyesp.spowlo.data.auth.AuthModel
+import com.bobbyesp.spowlo.domain.spotify.web_api.auth.SpotifyPkceLoginActivityImpl
+import com.bobbyesp.spowlo.domain.spotify.web_api.utilities.guardValidSpotifyApi
+import com.bobbyesp.spowlo.presentation.MainActivity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class SearcherViewModel @Inject constructor() : ViewModel() {
+ private val mutableStateFlow = MutableStateFlow(DownloaderViewState())
+ val stateFlow = mutableStateFlow.asStateFlow()
+ private var currentJob: Job? = null
+
+ private val _searchQuery = mutableStateOf("")
+ val searchQuery: State = _searchQuery
+
+
+ data class DownloaderViewState(
+ val spotUrl: String = "",
+ val ytUrl: String = "",
+ val progress: Float = 0f,
+ val isDownloading: Boolean = false,
+ val isCancelled: Boolean = false,
+ val songTitle: String = "",
+ val songArtist: String = "",
+ val isDownloadError: Boolean = false,
+ val debugMode: Boolean = false,
+ val showDownloadSettingDialog: Boolean = false,
+ val downloadingTaskId: String = "",
+ val isUrlSharingTriggered: Boolean = false,
+ val drawerState: Boolean = false,
+ val logged: Boolean = false,
+ val loaded: Boolean = false,
+ val recentBroadcasts: List = mutableListOf(),
+ val listOfTracks: List