diff --git a/.kotlin/sessions/kotlin-compiler-204136539978830062.salive b/.kotlin/sessions/kotlin-compiler-204136539978830062.salive deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/songlib/core/utils/AppConstants.kt b/app/src/main/java/com/songlib/core/utils/AppConstants.kt index ccf709c..1e97c34 100644 --- a/app/src/main/java/com/songlib/core/utils/AppConstants.kt +++ b/app/src/main/java/com/songlib/core/utils/AppConstants.kt @@ -24,9 +24,9 @@ object PrefConstants { const val INITIAL_BOOKS = "initialBooks" const val SELECTED_BOOKS = "selectedBooks" - const val DATA_SELECTED = "dataSelected" - const val DATA_LOADED = "dataLoaded" - const val SELECT_AFRESH = "selectAfresh" + const val IS_DATA_SELECTED = "dataSelected" + const val IS_DATA_LOADED = "dataLoaded" + const val SELECT_A_FRESH = "selectAfresh" const val IS_PRO_USER = "isProUser" const val INSTALL_DATE = "install_date" const val REVIEW_REQUESTED = "review_requested" diff --git a/app/src/main/java/com/songlib/data/sources/local/AppDatabase.kt b/app/src/main/java/com/songlib/data/sources/local/AppDatabase.kt index 835f13e..12bba16 100644 --- a/app/src/main/java/com/songlib/data/sources/local/AppDatabase.kt +++ b/app/src/main/java/com/songlib/data/sources/local/AppDatabase.kt @@ -10,11 +10,11 @@ import com.songlib.data.sources.local.daos.* version = 2, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { - abstract fun bookDao(): BookDao - abstract fun historyDao(): HistoryDao - abstract fun listingDao(): ListingDao - abstract fun searchDao(): SearchDao - abstract fun songDao(): SongDao + abstract fun booksDao(): BookDao + abstract fun historiesDao(): HistoryDao + abstract fun listingsDao(): ListingDao + abstract fun searchesDao(): SearchDao + abstract fun songsDao(): SongDao companion object { @Volatile diff --git a/app/src/main/java/com/songlib/data/sources/local/daos/BookDao.kt b/app/src/main/java/com/songlib/data/sources/local/daos/BookDao.kt index 445efc3..d8d1e0d 100644 --- a/app/src/main/java/com/songlib/data/sources/local/daos/BookDao.kt +++ b/app/src/main/java/com/songlib/data/sources/local/daos/BookDao.kt @@ -12,6 +12,9 @@ interface BookDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(book: Book) + @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) + suspend fun insertAll(books: List) + @Update() fun update(book: Book) diff --git a/app/src/main/java/com/songlib/data/sources/local/daos/SongDao.kt b/app/src/main/java/com/songlib/data/sources/local/daos/SongDao.kt index adf45d4..c39feef 100644 --- a/app/src/main/java/com/songlib/data/sources/local/daos/SongDao.kt +++ b/app/src/main/java/com/songlib/data/sources/local/daos/SongDao.kt @@ -15,6 +15,9 @@ interface SongDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(song: Song) + @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) + suspend fun insertAll(songs: List) + @Update() fun update(song: Song) diff --git a/app/src/main/java/com/songlib/domain/repos/ListingRepo.kt b/app/src/main/java/com/songlib/domain/repos/ListingRepo.kt index 94234f0..1458f48 100644 --- a/app/src/main/java/com/songlib/domain/repos/ListingRepo.kt +++ b/app/src/main/java/com/songlib/domain/repos/ListingRepo.kt @@ -10,16 +10,16 @@ import javax.inject.* @Singleton class ListingRepo @Inject constructor(context: Context) { - private var listingDao: ListingDao? + private var listingsDao: ListingDao? init { val db = AppDatabase.getDatabase(context) - listingDao = db?.listingDao() + listingsDao = db?.listingsDao() } suspend fun fetchListings(parent: Int): List { return withContext(Dispatchers.IO) { - val allListings = listingDao?.getAll(parent) ?: emptyList() + val allListings = listingsDao?.getAll(parent) ?: emptyList() allListings.map { listing -> ListingUi( @@ -29,7 +29,7 @@ class ListingRepo @Inject constructor(context: Context) { song = listing.song, created = listing.created, modified = listing.modified, - songCount = listingDao?.countSongs(listing.id) ?: 0, + songCount = listingsDao?.countSongs(listing.id) ?: 0, updatedAgo = listing.modified.toLongOrNull()?.toTimeAgo() ?: "" ) } @@ -46,21 +46,21 @@ class ListingRepo @Inject constructor(context: Context) { created = currentTime, modified = currentTime ) - listingDao?.insert(newListing) + listingsDao?.insert(newListing) } } suspend fun saveListItem(parent: ListingUi, song: Int) { withContext(Dispatchers.IO) { val currentTime = System.currentTimeMillis().toString() - listingDao?.insert(Listing( + listingsDao?.insert(Listing( parent = parent.id, title = "", song = song, created = currentTime, modified = currentTime )) - listingDao?.update(Listing( + listingsDao?.update(Listing( parent = parent.id, title = parent.title, song = song, @@ -73,7 +73,7 @@ class ListingRepo @Inject constructor(context: Context) { suspend fun updateListing(listing: ListingUi) { val currentTime = System.currentTimeMillis().toString() withContext(Dispatchers.IO) { - listingDao?.update(Listing( + listingsDao?.update(Listing( parent = listing.id, title = listing.title, song = listing.song, @@ -85,13 +85,13 @@ class ListingRepo @Inject constructor(context: Context) { suspend fun deleteById(listId: Int) { withContext(Dispatchers.IO) { - listingDao?.deleteById(listId) + listingsDao?.deleteById(listId) } } suspend fun deleteAllListings() { withContext(Dispatchers.IO) { - listingDao?.deleteAll() + listingsDao?.deleteAll() } } diff --git a/app/src/main/java/com/songlib/domain/repos/PrefsRepo.kt b/app/src/main/java/com/songlib/domain/repos/PrefsRepo.kt index cdee786..94ddf8d 100644 --- a/app/src/main/java/com/songlib/domain/repos/PrefsRepo.kt +++ b/app/src/main/java/com/songlib/domain/repos/PrefsRepo.kt @@ -22,20 +22,48 @@ class PrefsRepo @Inject constructor( set(value) = prefs.edit { putString(PrefConstants.SELECTED_BOOKS, value) } var isDataSelected: Boolean - get() = prefs.getBoolean(PrefConstants.DATA_SELECTED, false) - set(value) = prefs.edit { putBoolean(PrefConstants.DATA_SELECTED, value) } + get() = { + val value = prefs.getBoolean(PrefConstants.IS_DATA_SELECTED, false) + value + }() + set(value) = { + prefs.edit { + putBoolean(PrefConstants.IS_DATA_SELECTED, value) + } + }() var selectAfresh: Boolean - get() = prefs.getBoolean(PrefConstants.SELECT_AFRESH, false) - set(value) = prefs.edit { putBoolean(PrefConstants.SELECT_AFRESH, value) } + get() = { + val value = prefs.getBoolean(PrefConstants.SELECT_A_FRESH, false) + value + }() + set(value) = { + prefs.edit { + putBoolean(PrefConstants.SELECT_A_FRESH, value) + } + }() var isProUser: Boolean - get() = prefs.getBoolean(PrefConstants.IS_PRO_USER, false) - set(value) = prefs.edit { putBoolean(PrefConstants.IS_PRO_USER, value) } + get() = { + val value = prefs.getBoolean(PrefConstants.IS_PRO_USER, false) + value + }() + set(value) = { + prefs.edit { + putBoolean(PrefConstants.IS_PRO_USER, value) + } + }() var isDataLoaded: Boolean - get() = prefs.getBoolean(PrefConstants.DATA_LOADED, false) - set(value) = prefs.edit { putBoolean(PrefConstants.DATA_LOADED, value) } + get() = { + val value = prefs.getBoolean(PrefConstants.IS_DATA_LOADED, false) + value + }() + set(value) = { + prefs.edit { + putBoolean(PrefConstants.IS_DATA_LOADED, value) + } + }() var appThemeMode: ThemeMode get() = ThemeMode.valueOf( @@ -45,8 +73,15 @@ class PrefsRepo @Inject constructor( set(value) = prefs.edit { putString(PrefConstants.THEME_MODE, value.name) } var horizontalSlides: Boolean - get() = prefs.getBoolean(PrefConstants.HORIZONTAL_SLIDES, false) - set(value) = prefs.edit { putBoolean(PrefConstants.HORIZONTAL_SLIDES, value) } + get() = { + val value = prefs.getBoolean(PrefConstants.HORIZONTAL_SLIDES, false) + value + }() + set(value) = { + prefs.edit { + putBoolean(PrefConstants.HORIZONTAL_SLIDES, value) + } + }() var lastAppOpenTime: Long get() = prefs.getLong(PrefConstants.LAST_APP_OPEN_TIME, 0L) diff --git a/app/src/main/java/com/songlib/domain/repos/SongBookRepo.kt b/app/src/main/java/com/songlib/domain/repos/SongBookRepo.kt index a3acc8a..7221a20 100644 --- a/app/src/main/java/com/songlib/domain/repos/SongBookRepo.kt +++ b/app/src/main/java/com/songlib/domain/repos/SongBookRepo.kt @@ -1,6 +1,7 @@ package com.songlib.domain.repos import android.content.* +import android.util.Log import com.songlib.data.models.* import com.songlib.data.sources.local.* import com.songlib.data.sources.local.daos.* @@ -8,47 +9,94 @@ import com.songlib.data.sources.remote.ApiService import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import javax.inject.* +import kotlin.collections.isNotEmpty +import kotlin.collections.map @Singleton class SongBookRepo @Inject constructor( context: Context, private val apiService: ApiService, ) { - private var bookDao: BookDao? - private var songDao: SongDao? + private var booksDao: BookDao? + private var songsDao: SongDao? init { val db = AppDatabase.getDatabase(context) - bookDao = db?.bookDao() - songDao = db?.songDao() + booksDao = db?.booksDao() + songsDao = db?.songsDao() } fun fetchRemoteBooks(): Flow> = flow { - val books = apiService.getBooks() - emit(books) + try { + val books = apiService.getBooks() + if (books.isNotEmpty()) { + emit(books) + } else { + Log.d("TAG", "⚠️ No books fetched from remote") + emit(emptyList()) + } + } catch (e: Exception) { + Log.e("TAG", "❌ Error fetching books: ${e.message}", e) + throw e + } } - fun fetchRemoteSongs(books: String): Flow> = flow { - val songs = apiService.getSongs(books) - emit(songs) + suspend fun fetchAndSaveSongs(bookIds: List) { + try { + val booksParam = bookIds.joinToString(",") + val songs = apiService.getSongs(booksParam) + Log.d("TAG", "✅ ${songs.size} songs fetched for books: $booksParam") + + if (songs.isNotEmpty()) { + saveSongs(songs) + } else { + Log.d("TAG", "⚠️ No songs fetched from remote") + } + } catch (e: Exception) { + Log.e("TAG", "❌ Error fetching songs: ${e.message}", e) + } } suspend fun saveBook(book: Book) { withContext(Dispatchers.IO) { - bookDao?.insert(book) + booksDao?.insert(book) } } - suspend fun saveSong(song: Song) { - withContext(Dispatchers.IO) { - songDao?.insert(song) + suspend fun saveBooks(books: List) { + if (books.isEmpty()) { + Log.d("TAG", "⚠️ No books to save") + return + } + + try { + booksDao?.insertAll(books) + Log.d("TAG", "✅ ${books.size} books saved successfully") + } catch (e: Exception) { + Log.e("TAG", "❌ Error saving books: ${e.message}", e) + throw e + } + } + + suspend fun saveSongs(songs: List) { + if (songs.isEmpty()) { + Log.d("TAG", "⚠️ No songs to save") + return + } + + try { + songsDao?.insertAll(songs) + Log.d("TAG", "✅ ${songs.size} songs saved successfully") + } catch (e: Exception) { + Log.e("TAG", "❌ Error saving songs: ${e.message}", e) + throw e } } suspend fun fetchLocalBooks(): List { var allBooks: List withContext(Dispatchers.IO) { - allBooks = bookDao?.getAll() ?: emptyList() + allBooks = booksDao?.getAll() ?: emptyList() } return allBooks } @@ -56,7 +104,7 @@ class SongBookRepo @Inject constructor( suspend fun fetchLocalSongs(): List { var allSongs: List withContext(Dispatchers.IO) { - allSongs = songDao?.getAll() ?: emptyList() + allSongs = songsDao?.getAll() ?: emptyList() } return allSongs } @@ -64,33 +112,33 @@ class SongBookRepo @Inject constructor( suspend fun fetchSong(songId: Int): Song { var song: Song withContext(Dispatchers.IO) { - song = songDao?.getSong(songId)!! + song = songsDao?.getSong(songId)!! } return song } suspend fun updateSong(song: Song) { withContext(Dispatchers.IO) { - songDao?.update(song) + songsDao?.update(song) } } suspend fun deleteById(bookId: Int) { withContext(Dispatchers.IO) { - bookDao?.deleteById(bookId) + booksDao?.deleteById(bookId) } } suspend fun deleteAllData() { withContext(Dispatchers.IO) { - bookDao?.deleteAll() - songDao?.deleteAll() + booksDao?.deleteAll() + songsDao?.deleteAll() } } suspend fun deleteByBookId(bookId: Int) { withContext(Dispatchers.IO) { - songDao?.deleteById(bookId) + songsDao?.deleteById(bookId) } } diff --git a/app/src/main/java/com/songlib/domain/repos/TrackingRepo.kt b/app/src/main/java/com/songlib/domain/repos/TrackingRepo.kt index c770849..7056819 100644 --- a/app/src/main/java/com/songlib/domain/repos/TrackingRepo.kt +++ b/app/src/main/java/com/songlib/domain/repos/TrackingRepo.kt @@ -11,31 +11,31 @@ import javax.inject.* class TrackingRepo @Inject constructor( context: Context, ) { - private var historyDao: HistoryDao? - private var searchDao: SearchDao? + private var historiesDao: HistoryDao? + private var searchesDao: SearchDao? init { val db = AppDatabase.getDatabase(context) - historyDao = db?.historyDao() - searchDao = db?.searchDao() + historiesDao = db?.historiesDao() + searchesDao = db?.searchesDao() } suspend fun saveHistory(history: History) { withContext(Dispatchers.IO) { - historyDao?.insert(history) + historiesDao?.insert(history) } } suspend fun saveSearch(search: Search) { withContext(Dispatchers.IO) { - searchDao?.insert(search) + searchesDao?.insert(search) } } suspend fun fetchHistories(): List { var allHistories: List withContext(Dispatchers.IO) { - allHistories = historyDao?.getAll() ?: emptyList() + allHistories = historiesDao?.getAll() ?: emptyList() } return allHistories } @@ -43,38 +43,38 @@ class TrackingRepo @Inject constructor( suspend fun fetchSearches(): List { var allSearches: List withContext(Dispatchers.IO) { - allSearches = searchDao?.getAll() ?: emptyList() + allSearches = searchesDao?.getAll() ?: emptyList() } return allSearches } suspend fun updateSearch(search: Search) { withContext(Dispatchers.IO) { - searchDao?.update(search) + searchesDao?.update(search) } } suspend fun deleteHistoryById(id: Int) { withContext(Dispatchers.IO) { - historyDao?.deleteById(id) + historiesDao?.deleteById(id) } } suspend fun deleteAllHistories() { withContext(Dispatchers.IO) { - historyDao?.deleteAll() + historiesDao?.deleteAll() } } suspend fun deleteSearchById(id: Int) { withContext(Dispatchers.IO) { - searchDao?.deleteById(id) + searchesDao?.deleteById(id) } } suspend fun deleteAllSearches() { withContext(Dispatchers.IO) { - searchDao?.deleteAll() + searchesDao?.deleteAll() } } diff --git a/app/src/main/java/com/songlib/presentation/components/listitems/SongBook.kt b/app/src/main/java/com/songlib/presentation/components/listitems/SongBook.kt index 72996bd..f40e84c 100644 --- a/app/src/main/java/com/songlib/presentation/components/listitems/SongBook.kt +++ b/app/src/main/java/com/songlib/presentation/components/listitems/SongBook.kt @@ -47,11 +47,11 @@ fun SongBook( ) { Text( text = buildAnnotatedString { - withStyle(style = SpanStyle(fontSize = 16.sp, color = txtColor)) { + withStyle(style = SpanStyle(fontSize = 18.sp, color = txtColor)) { append(refineTitle(item.data.title)) } append(" ") - withStyle(style = SpanStyle(fontSize = 12.sp, color = txtColor.copy(alpha = 0.7f))) { + withStyle(style = SpanStyle(fontSize = 14.sp, color = txtColor.copy(alpha = 0.7f))) { append("(${item.data.songs})") } }, diff --git a/app/src/main/java/com/songlib/presentation/home/components/HomeAppBar.kt b/app/src/main/java/com/songlib/presentation/home/components/HomeAppBar.kt index e965aa6..30ca83c 100644 --- a/app/src/main/java/com/songlib/presentation/home/components/HomeAppBar.kt +++ b/app/src/main/java/com/songlib/presentation/home/components/HomeAppBar.kt @@ -4,9 +4,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalContext import com.songlib.data.models.Song -import com.songlib.domain.repos.PrefsRepo import com.songlib.presentation.components.action.AppTopBar import com.songlib.presentation.components.general.QuickFormDialog import com.songlib.presentation.home.HomeViewModel @@ -21,8 +19,6 @@ fun HomeSearchAppBar( onShareClick: () -> Unit, onClearSelection: () -> Unit, ) { - val context = LocalContext.current - val prefs = remember { PrefsRepo(context) } var showAddDialog by remember { mutableStateOf(false) } var showListingSheet by remember { mutableStateOf(false) } val isProUser by viewModel.isProUser.collectAsState() @@ -63,8 +59,7 @@ fun HomeSearchAppBar( } AppTopBar( -// title = if (selectedSongs.isEmpty()) "SongLib" else "${selectedSongs.size} selected", - title = if (prefs.isDataLoaded) "True" else "False", + title = "SongLib", actions = { if (selectedSongs.isEmpty()) { IconButton(onClick = onSearchClick) { diff --git a/app/src/main/java/com/songlib/presentation/navigation/AppNavHost.kt b/app/src/main/java/com/songlib/presentation/navigation/AppNavHost.kt index a4ee147..72a197f 100644 --- a/app/src/main/java/com/songlib/presentation/navigation/AppNavHost.kt +++ b/app/src/main/java/com/songlib/presentation/navigation/AppNavHost.kt @@ -14,10 +14,8 @@ import com.songlib.presentation.listing.ListingViewModel import com.songlib.presentation.listing.view.ListingScreen import com.songlib.presentation.presenter.PresenterViewModel import com.songlib.presentation.presenter.view.PresenterScreen -import com.songlib.presentation.selection.step1.Step1ViewModel -import com.songlib.presentation.selection.step1.view.Step1Screen -import com.songlib.presentation.selection.step2.Step2ViewModel -import com.songlib.presentation.selection.step2.view.Step2Screen +import com.songlib.presentation.selection.SelectionViewModel +import com.songlib.presentation.selection.view.SelectionScreen import com.songlib.presentation.settings.SettingsViewModel import com.songlib.presentation.settings.view.SettingsScreen import com.songlib.presentation.splash.SplashViewModel @@ -39,23 +37,15 @@ fun AppNavHost( SplashScreen(navController = navController, viewModel = viewModel) } - composable(Routes.STEP_1) { - val viewModel: Step1ViewModel = hiltViewModel() - Step1Screen( + composable(Routes.SELECTION) { + val viewModel: SelectionViewModel = hiltViewModel() + SelectionScreen( navController = navController, viewModel = viewModel, themeRepo = themeRepo, ) } - composable(Routes.STEP_2) { - val viewModel: Step2ViewModel = hiltViewModel() - Step2Screen( - navController = navController, - viewModel = viewModel, - ) - } - composable(Routes.HOME) { val viewModel: HomeViewModel = hiltViewModel() HomeScreen( diff --git a/app/src/main/java/com/songlib/presentation/navigation/Routes.kt b/app/src/main/java/com/songlib/presentation/navigation/Routes.kt index 76e2c55..dca0f51 100644 --- a/app/src/main/java/com/songlib/presentation/navigation/Routes.kt +++ b/app/src/main/java/com/songlib/presentation/navigation/Routes.kt @@ -2,8 +2,7 @@ package com.songlib.presentation.navigation object Routes { const val SPLASH = "splash" - const val STEP_1 = "step1" - const val STEP_2 = "step2" + const val SELECTION = "selection" const val HOME = "home" const val PRESENTER = "presenter" const val LISTING = "listing" diff --git a/app/src/main/java/com/songlib/presentation/selection/step1/Step1ViewModel.kt b/app/src/main/java/com/songlib/presentation/selection/SelectionViewModel.kt similarity index 74% rename from app/src/main/java/com/songlib/presentation/selection/step1/Step1ViewModel.kt rename to app/src/main/java/com/songlib/presentation/selection/SelectionViewModel.kt index 3c81e9b..0b6018b 100644 --- a/app/src/main/java/com/songlib/presentation/selection/step1/Step1ViewModel.kt +++ b/app/src/main/java/com/songlib/presentation/selection/SelectionViewModel.kt @@ -1,26 +1,19 @@ -package com.songlib.presentation.selection.step1 +package com.songlib.presentation.selection import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* import com.songlib.data.models.Book -import com.songlib.domain.entity.Selectable -import com.songlib.domain.entity.UiState -import com.songlib.domain.repos.PrefsRepo -import com.songlib.domain.repos.SongBookRepo -import com.songlib.domain.repos.SubsRepo +import com.songlib.domain.entity.* +import com.songlib.domain.repos.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.* import retrofit2.HttpException import javax.inject.Inject @HiltViewModel -class Step1ViewModel @Inject constructor( +class SelectionViewModel @Inject constructor( private val prefsRepo: PrefsRepo, private val subsRepo: SubsRepo, private val songbkRepo: SongBookRepo, @@ -51,10 +44,10 @@ class Step1ViewModel @Inject constructor( viewModelScope.launch { songbkRepo.fetchRemoteBooks().catch { exception -> - Log.d("TAG", "fetching books") + Log.d("TAG", "fetching books error") val errorMessage = when (exception) { - is HttpException -> "We're sorry. We can't access the songbooks at the moment due to a HTTP Error: ${exception.code()} error on our server. Kindly try again a little later." - else -> "We're sorry. We can't access the songbooks at the moment due to a ${exception.message} error on our server. Kindly try again a little later." + is HttpException -> "Oops! We can't access the songbooks at the moment due to a HTTP Error: ${exception.code()} error on our server. Kindly try again a little later." + else -> "Oops! We can't access the songbooks at the moment due to a ${exception.message} error on our server. Kindly try again a little later." } Log.d("TAG", errorMessage) _uiState.tryEmit(UiState.Error(errorMessage)) @@ -89,9 +82,9 @@ class Step1ViewModel @Inject constructor( private fun saveBooks(books: List) { _uiState.tryEmit(UiState.Saving) - Log.d("TAG", "saving books") + Log.d("TAG", "saving ${books.size} books") - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { try { if (prefsRepo.selectAfresh) { val existingIds = getSelectedIds() @@ -100,17 +93,17 @@ class Step1ViewModel @Inject constructor( val booksToInsert = books.filter { it.bookId !in existingIds } val idsToDelete = existingIds - newIds - booksToInsert.forEach { songbkRepo.saveBook(it) } idsToDelete.forEach { songbkRepo.deleteById(it) } + booksToInsert.forEach { songbkRepo.saveBook(it) } prefsRepo.selectedBooks = newIds.joinToString(",") } else { - books.forEach { songbkRepo.saveBook(it) } + prefsRepo.isDataSelected = true + songbkRepo.saveBooks(books) prefsRepo.selectedBooks = books.joinToString(",") { it.bookId.toString() } } - prefsRepo.isDataSelected = true - _uiState.emit(UiState.Saved) + fetchRemoteSongs(books.map { it.bookId }) } catch (e: Exception) { Log.e("SaveBooks", "Failed to save books", e) _uiState.emit(UiState.Error("Failed to save books: ${e.message}")) @@ -118,6 +111,23 @@ class Step1ViewModel @Inject constructor( } } + private suspend fun fetchRemoteSongs(bookIds: List) { + try { + Log.d("TAG", "Starting song fetch for ${bookIds.size} books") + withContext(Dispatchers.IO) { + songbkRepo.fetchAndSaveSongs(bookIds) + } + + prefsRepo.isDataLoaded = true + Log.d("TAG", "Song fetch and save completed") + _uiState.tryEmit(UiState.Saved) + + } catch (e: Exception) { + Log.e("TAG", "Song fetch failed: ${e.message}", e) + _uiState.tryEmit(UiState.Saved) + } + } + fun toggleBookSelection(book: Selectable) { val currentSelectedCount = _books.value.count { it.isSelected } val isCurrentlySelected = book.isSelected @@ -129,7 +139,7 @@ class Step1ViewModel @Inject constructor( return } - if (currentSelectedCount >= 3) { + if (currentSelectedCount >= 4) { _pendingBookSelection.value = book _showUpgradeDialog.value = true } else { @@ -142,12 +152,10 @@ class Step1ViewModel @Inject constructor( fun onUpgradeProceed() { _showUpgradeDialog.value = false _pendingBookSelection.value = null -// refreshSubscription(on) } fun onUpgradeDismis() { _showUpgradeDialog.value = false _pendingBookSelection.value = null } - } \ No newline at end of file diff --git a/app/src/main/java/com/songlib/presentation/selection/step1/components/Step1Fab.kt b/app/src/main/java/com/songlib/presentation/selection/components/SelectionFab.kt similarity index 90% rename from app/src/main/java/com/songlib/presentation/selection/step1/components/Step1Fab.kt rename to app/src/main/java/com/songlib/presentation/selection/components/SelectionFab.kt index 0b05896..95970bd 100644 --- a/app/src/main/java/com/songlib/presentation/selection/step1/components/Step1Fab.kt +++ b/app/src/main/java/com/songlib/presentation/selection/components/SelectionFab.kt @@ -1,4 +1,4 @@ -package com.songlib.presentation.selection.step1.components +package com.songlib.presentation.selection.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -6,11 +6,11 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import com.songlib.data.models.Book import com.songlib.presentation.components.general.* -import com.songlib.presentation.selection.step1.Step1ViewModel +import com.songlib.presentation.selection.SelectionViewModel @Composable fun Step1Fab( - viewModel: Step1ViewModel, + viewModel: SelectionViewModel, onSaveConfirmed: (List) -> Unit ) { var showConfirmDialog by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/songlib/presentation/selection/step2/Step2ViewModel.kt b/app/src/main/java/com/songlib/presentation/selection/step2/Step2ViewModel.kt deleted file mode 100644 index 1cd614e..0000000 --- a/app/src/main/java/com/songlib/presentation/selection/step2/Step2ViewModel.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.songlib.presentation.selection.step2 - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.songlib.data.models.Song -import com.songlib.domain.entity.UiState -import com.songlib.domain.repos.PrefsRepo -import com.songlib.domain.repos.SongBookRepo -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.launch -import retrofit2.HttpException -import javax.inject.Inject - -@HiltViewModel -class Step2ViewModel @Inject constructor( - private val prefsRepo: PrefsRepo, - private val songbkRepo: SongBookRepo, -) : ViewModel() { - private val _uiState: MutableStateFlow = MutableStateFlow(UiState.Loading) - val uiState: StateFlow = _uiState.asStateFlow() - - private val _progress = MutableStateFlow(0) - val progress: StateFlow = _progress.asStateFlow() - - private val _songs = MutableStateFlow>(emptyList()) - val songs: StateFlow> get() = _songs - - fun fetchSongs() { - _uiState.tryEmit(UiState.Loading) - viewModelScope.launch { - val books = prefsRepo.selectedBooks - songbkRepo.fetchRemoteSongs(books).catch { exception -> - Log.d("TAG", "fetching songs") - val errorMessage = when (exception) { - is HttpException -> "We're sorry. We can't access the songs at the moment due to a HTTP Error: ${exception.code()} error on our server. Kindly try again a little later." - else -> "We're sorry. We can't access the songs at the moment due to a ${exception.message} error on our server. Kindly try again a little later." - } - Log.d("TAG", errorMessage) - _uiState.tryEmit(UiState.Error(errorMessage)) - }.collect { respData -> - _songs.emit(respData) - Log.d("TAG", "${_songs.value.size} songs fetched") - _uiState.tryEmit(UiState.Loaded) - } - } - } - - fun saveSongs() { - Log.d("TAG", "Saving songs") - val songs = _songs.value - val total = songs.size - _uiState.tryEmit(UiState.Saving) - - viewModelScope.launch(Dispatchers.IO) { - try { - if (prefsRepo.selectAfresh) { - // Get old and new book IDs - val oldBookIds = prefsRepo.initialBooks - .split(",") - .mapNotNull { it.toIntOrNull() } - .toSet() - - val newBookIds = prefsRepo.selectedBooks - .split(",") - .mapNotNull { it.toIntOrNull() } - .toSet() - - val removedBookIds = oldBookIds - newBookIds - removedBookIds.forEach { bookId -> - songbkRepo.deleteByBookId(bookId) - } - - songs.forEachIndexed { index, song -> - if (song.book in newBookIds) { - songbkRepo.saveSong(song) - } - val percent = ((index + 1).toFloat() / total * 100).toInt() - _progress.emit(percent) - } - } else { - songs.forEachIndexed { index, song -> - songbkRepo.saveSong(song) - val percent = ((index + 1).toFloat() / total * 100).toInt() - _progress.emit(percent) - } - } - - prefsRepo.isDataLoaded = true - _uiState.emit(UiState.Saved) - } catch (e: Exception) { - prefsRepo.isDataLoaded = false - Log.e("SaveSongs", "Failed to save songs", e) - _uiState.emit(UiState.Error("Failed to save songs: ${e.message}")) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/songlib/presentation/selection/step2/view/Step2Screen.kt b/app/src/main/java/com/songlib/presentation/selection/step2/view/Step2Screen.kt deleted file mode 100644 index 4e07020..0000000 --- a/app/src/main/java/com/songlib/presentation/selection/step2/view/Step2Screen.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.songlib.presentation.selection.step2.view - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.* -import androidx.navigation.NavHostController -import com.songlib.domain.entity.UiState -import com.songlib.presentation.components.indicators.* -import com.songlib.presentation.navigation.Routes -import com.songlib.presentation.selection.step2.Step2ViewModel - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun Step2Screen( - navController: NavHostController, - viewModel: Step2ViewModel, -) { - var fetchData by rememberSaveable { mutableIntStateOf(0) } - - if (fetchData == 0) { - viewModel.fetchSongs() - fetchData++ - } - - val uiState by viewModel.uiState.collectAsState() - val progress by viewModel.progress.collectAsState(initial = 0) - - LaunchedEffect(uiState) { - if (uiState == UiState.Saved) { - navController.navigate(Routes.HOME) - } - } - - Scaffold( - content = { paddingValues -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .background(color = MaterialTheme.colorScheme.surface) - ) { - when (uiState) { - is UiState.Error -> ErrorState( - message = (uiState as UiState.Error).message, - retryAction = { viewModel.fetchSongs() } - ) - - is UiState.Loading -> LoadingState( - title = "Loading Songs ...", - fileName = "loading-hand", - ) - - is UiState.Saving -> - LoadingState( - title = "Saving your songs ...", - showProgress = true, - progressValue = progress, - fileName = "cloud-download", - ) - - is UiState.Loaded -> viewModel.saveSongs() - else -> EmptyState() - } - } - }, - ) -} diff --git a/app/src/main/java/com/songlib/presentation/selection/step1/view/Step1Content.kt b/app/src/main/java/com/songlib/presentation/selection/view/SelectionContent.kt similarity index 89% rename from app/src/main/java/com/songlib/presentation/selection/step1/view/Step1Content.kt rename to app/src/main/java/com/songlib/presentation/selection/view/SelectionContent.kt index 1017553..ac70f53 100644 --- a/app/src/main/java/com/songlib/presentation/selection/step1/view/Step1Content.kt +++ b/app/src/main/java/com/songlib/presentation/selection/view/SelectionContent.kt @@ -1,4 +1,4 @@ -package com.songlib.presentation.selection.step1.view +package com.songlib.presentation.selection.view import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -14,7 +14,7 @@ import com.songlib.domain.entity.* import com.songlib.presentation.components.listitems.SongBook @Composable -fun Step1Content( +fun SelectionContent( books: List>, onBookClick: (Selectable) -> Unit, modifier: Modifier = Modifier @@ -36,7 +36,7 @@ fun Step1Content( SongBook( item = book, onClick = { onBookClick(book) }, - modifier = Modifier.height(60.dp) + modifier = Modifier.height(90.dp) ) } } @@ -45,8 +45,8 @@ fun Step1Content( @Preview(showBackground = true) @Composable -fun Step1ContentPreview() { - Step1Content( +fun SelectionContentPreview() { + SelectionContent( books = SampleSelectableBooks, onBookClick = {}, modifier = Modifier.fillMaxSize() diff --git a/app/src/main/java/com/songlib/presentation/selection/step1/view/Step1Screen.kt b/app/src/main/java/com/songlib/presentation/selection/view/SelectionScreen.kt similarity index 87% rename from app/src/main/java/com/songlib/presentation/selection/step1/view/Step1Screen.kt rename to app/src/main/java/com/songlib/presentation/selection/view/SelectionScreen.kt index 6a1f0a6..82a5f5c 100644 --- a/app/src/main/java/com/songlib/presentation/selection/step1/view/Step1Screen.kt +++ b/app/src/main/java/com/songlib/presentation/selection/view/SelectionScreen.kt @@ -1,4 +1,4 @@ -package com.songlib.presentation.selection.step1.view +package com.songlib.presentation.selection.view import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons @@ -7,25 +7,22 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.window.* import androidx.navigation.NavHostController -import com.revenuecat.purchases.ui.revenuecatui.Paywall -import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions +import com.revenuecat.purchases.ui.revenuecatui.* import com.songlib.domain.entity.UiState -import com.songlib.domain.repos.ThemeRepository -import com.songlib.domain.repos.ThemeSelectorDialog +import com.songlib.domain.repos.* import com.songlib.presentation.components.action.AppTopBar import com.songlib.presentation.components.indicators.* import com.songlib.presentation.navigation.Routes -import com.songlib.presentation.selection.step1.Step1ViewModel -import com.songlib.presentation.selection.step1.components.Step1Fab +import com.songlib.presentation.selection.SelectionViewModel +import com.songlib.presentation.selection.components.Step1Fab @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Step1Screen( +fun SelectionScreen( navController: NavHostController, - viewModel: Step1ViewModel, + viewModel: SelectionViewModel, themeRepo: ThemeRepository ) { var fetchData by rememberSaveable { mutableIntStateOf(0) } @@ -45,14 +42,14 @@ fun Step1Screen( LaunchedEffect(uiState) { if (uiState == UiState.Saved) { - navController.navigate(Routes.STEP_2) + navController.navigate(Routes.HOME) } } if (showUpgradeDialog) { AlertDialog( onDismissRequest = { viewModel.onUpgradeDismis() }, - title = { Text("You selected more than 3 Songbooks...") }, + title = { Text("You selected more than 4 Songbooks...") }, text = { Text("Please purchase a subscription if you want to have more than 3 songbooks in your collection.") }, @@ -145,7 +142,7 @@ fun Step1Screen( ) is UiState.Loaded -> { - Step1Content( + SelectionContent( books = books, onBookClick = { viewModel.toggleBookSelection(it) }, modifier = Modifier.padding(paddingValues) diff --git a/app/src/main/java/com/songlib/presentation/splash/SplashViewModel.kt b/app/src/main/java/com/songlib/presentation/splash/SplashViewModel.kt index 88ab489..937ce4c 100644 --- a/app/src/main/java/com/songlib/presentation/splash/SplashViewModel.kt +++ b/app/src/main/java/com/songlib/presentation/splash/SplashViewModel.kt @@ -18,8 +18,21 @@ class SplashViewModel @Inject constructor( private val _isLoading = MutableStateFlow(true) val isLoading: StateFlow = _isLoading.asStateFlow() + private val _selectAfresh = MutableStateFlow(prefsRepo.selectAfresh) + val selectAfresh: StateFlow = _selectAfresh.asStateFlow() + + private val _isDataLoaded = MutableStateFlow(prefsRepo.isDataLoaded) + val isDataLoaded: StateFlow = _isDataLoaded.asStateFlow() + + private val _isDataSelected = MutableStateFlow(prefsRepo.isDataSelected) + val isDataSelected: StateFlow = _isDataSelected.asStateFlow() + fun initializeApp(context: Context) { viewModelScope.launch { + _selectAfresh.value = prefsRepo.selectAfresh + _isDataLoaded.value = prefsRepo.isDataLoaded + _isDataSelected.value = prefsRepo.isDataSelected + try { if (NetworkUtils.isNetworkAvailable(context)) { checkSubscriptionAndTime(true) diff --git a/app/src/main/java/com/songlib/presentation/splash/view/SplashScreen.kt b/app/src/main/java/com/songlib/presentation/splash/view/SplashScreen.kt index 6ee9e0f..9dc8484 100644 --- a/app/src/main/java/com/songlib/presentation/splash/view/SplashScreen.kt +++ b/app/src/main/java/com/songlib/presentation/splash/view/SplashScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.* import androidx.navigation.NavHostController import com.songlib.R -import com.songlib.domain.repos.PrefsRepo import com.songlib.presentation.navigation.Routes import com.songlib.presentation.splash.components.* import com.songlib.presentation.splash.SplashViewModel @@ -25,24 +24,29 @@ fun SplashScreen( viewModel: SplashViewModel, ) { val context = LocalContext.current - val prefs = remember { PrefsRepo(context) } val isLoading by viewModel.isLoading.collectAsState() + val selectAfresh by viewModel.selectAfresh.collectAsState() + val isDataLoaded by viewModel.isDataLoaded.collectAsState() + val isDataSelected by viewModel.isDataSelected.collectAsState() LaunchedEffect(Unit) { viewModel.initializeApp(context) } - LaunchedEffect(Unit) { - delay(3000) + LaunchedEffect(isLoading, isDataLoaded) { + if (!isLoading) { + delay(3000) +// +// val nextRoute = if (selectAfresh || isDataSelected) { +// Routes.SELECTION +// } else if (isDataLoaded) { +// Routes.HOME +// } else { +// Routes.SELECTION +// } - val nextRoute = when { - prefs.selectAfresh -> Routes.STEP_1 - prefs.isDataLoaded -> Routes.HOME - prefs.isDataSelected -> Routes.STEP_2 - else -> Routes.STEP_1 - } + val nextRoute = if (isDataLoaded) Routes.HOME else Routes.SELECTION - if (!isLoading) { navController.navigate(nextRoute) { popUpTo(Routes.SPLASH) { inclusive = true } } diff --git a/gradle/config/config.properties b/gradle/config/config.properties index b7d2dd4..081aebb 100644 --- a/gradle/config/config.properties +++ b/gradle/config/config.properties @@ -1,5 +1,5 @@ applicationId=com.songlib -versionName=1.0.822 -versionCode=822 +versionName=1.0.824 +versionCode=824 targetSdk=35 minSdk=24 \ No newline at end of file