diff --git a/app/src/main/java/com/allsoftdroid/audiobook/presentation/MainActivity.kt b/app/src/main/java/com/allsoftdroid/audiobook/presentation/MainActivity.kt index 1bf326c9..aa1ce0cc 100644 --- a/app/src/main/java/com/allsoftdroid/audiobook/presentation/MainActivity.kt +++ b/app/src/main/java/com/allsoftdroid/audiobook/presentation/MainActivity.kt @@ -259,7 +259,6 @@ class MainActivity : BaseActivity() { layoutParams = layout } } - } private fun performAction(event: AudioPlayerEvent){ @@ -321,7 +320,6 @@ class MainActivity : BaseActivity() { stopAudioService() disposables.dispose() downloader.Destroy() - AppModule.unloadModule() } private fun stopAudioService(){ diff --git a/common/src/main/res/navigation/nav_graph.xml b/common/src/main/res/navigation/nav_graph.xml index f8221fd6..dcd89cb9 100644 --- a/common/src/main/res/navigation/nav_graph.xml +++ b/common/src/main/res/navigation/nav_graph.xml @@ -38,8 +38,11 @@ + + @Query("SELECT * FROM metadata_table where metadata_id=:bookId") + fun getMetadataNonLive( bookId : String):DatabaseMetadataEntity + /** * Get album details for the specified audio book * @param metadata_id unique id given to audio book @@ -50,6 +53,9 @@ interface MetadataDao{ @Query("SELECT * FROM MediaTrack_Table where track_album_id=:metadata_id and format like '%' || :formatContains || '%'") fun getTrackDetails(metadata_id:String,formatContains:String):LiveData> + @Query("SELECT * FROM MediaTrack_Table where track_album_id=:metadata_id and format like '%' || :formatContains || '%'") + fun getTrackDetailsNonLive(metadata_id:String,formatContains:String):List + /** * get list of media VBR track files for the given album id . here album id is same as metadata id so we will * use complex sql queries to get VBR files diff --git a/feature_book/src/main/java/com/allsoftdroid/feature_book/presentation/AudioBookListFragment.kt b/feature_book/src/main/java/com/allsoftdroid/feature_book/presentation/AudioBookListFragment.kt index 05a2c654..a0615abf 100644 --- a/feature_book/src/main/java/com/allsoftdroid/feature_book/presentation/AudioBookListFragment.kt +++ b/feature_book/src/main/java/com/allsoftdroid/feature_book/presentation/AudioBookListFragment.kt @@ -274,9 +274,4 @@ class AudioBookListFragment : BaseUIFragment(){ } } } - - override fun onDestroy() { - super.onDestroy() - FeatureBookModule.unLoadModules() - } } \ No newline at end of file diff --git a/feature_book_details/src/main/java/com/allsoftdroid/feature/book_details/di/BookDetailsModule.kt b/feature_book_details/src/main/java/com/allsoftdroid/feature/book_details/di/BookDetailsModule.kt index 91a85dc0..77caee32 100644 --- a/feature_book_details/src/main/java/com/allsoftdroid/feature/book_details/di/BookDetailsModule.kt +++ b/feature_book_details/src/main/java/com/allsoftdroid/feature/book_details/di/BookDetailsModule.kt @@ -115,7 +115,7 @@ object BookDetailsModule { factory { MetadataRepositoryImpl( - metadataDao = get(), + metadataDao = get(named(name = METADATA_DAO)), bookId = getProperty(PROPERTY_BOOK_ID), metadataDataSource = get(), saveInDatabase = get(named(name = METADATA_DATABASE))) as IMetadataRepository @@ -123,7 +123,7 @@ object BookDetailsModule { factory { TrackListRepositoryImpl( - metadataDao = get(), + metadataDao = get(named(name = METADATA_DAO)), bookId = getProperty(PROPERTY_BOOK_ID) ) as ITrackListRepository } @@ -158,7 +158,7 @@ object BookDetailsModule { AudioBookDatabase.getDatabase(get()).listenLaterDao() } - single { + single(named(name = METADATA_DAO)) { AudioBookDatabase.getDatabase(get()).metadataDao() } @@ -167,7 +167,7 @@ object BookDetailsModule { } single(named(name = METADATA_DATABASE)) { - SaveMetadataInDatabase.setup(metadataDao = get()) as SaveInDatabase + SaveMetadataInDatabase.setup(metadataDao = get(named(name = METADATA_DAO))) as SaveInDatabase } single { @@ -185,4 +185,5 @@ object BookDetailsModule { const val PROPERTY_BOOK_ID = "bookDetails_book_id" private const val METADATA_DATABASE = "SaveMetadataInDatabase" + private const val METADATA_DAO ="MetadataDao_BookDetailsModule" } \ No newline at end of file diff --git a/feature_book_details/src/test/java/com/allsoftdroid/feature/book_details/utils/FakeAudioDataSource.kt b/feature_book_details/src/test/java/com/allsoftdroid/feature/book_details/utils/FakeAudioDataSource.kt index 0efd915f..fd6138a6 100644 --- a/feature_book_details/src/test/java/com/allsoftdroid/feature/book_details/utils/FakeAudioDataSource.kt +++ b/feature_book_details/src/test/java/com/allsoftdroid/feature/book_details/utils/FakeAudioDataSource.kt @@ -2,6 +2,7 @@ package com.allsoftdroid.feature.book_details.utils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.allsoftdroid.common.test.getOrAwaitValue import com.allsoftdroid.database.metadataCacheDB.MetadataDao import com.allsoftdroid.database.metadataCacheDB.entity.DatabaseAlbumEntity import com.allsoftdroid.database.metadataCacheDB.entity.DatabaseMetadataEntity @@ -17,6 +18,10 @@ class FakeMetadataSource(private val _metadataLiveData: MutableLiveData { return _albumEntity @@ -34,6 +39,13 @@ class FakeMetadataSource(private val _metadataLiveData: MutableLiveData { + return _tracks.getOrAwaitValue() + } + override fun getTrackDetailsVBR(metadata_id: String): LiveData> { return _tracks } diff --git a/feature_downloader/src/main/res/drawable/ic_close_circle.xml b/feature_downloader/src/main/res/drawable/ic_close_circle.xml index 8c3303c8..6a920e0a 100644 --- a/feature_downloader/src/main/res/drawable/ic_close_circle.xml +++ b/feature_downloader/src/main/res/drawable/ic_close_circle.xml @@ -4,5 +4,5 @@ android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + \ No newline at end of file diff --git a/feature_downloader/src/main/res/drawable/ic_delete.xml b/feature_downloader/src/main/res/drawable/ic_delete.xml index ef65fba8..c8b20470 100644 --- a/feature_downloader/src/main/res/drawable/ic_delete.xml +++ b/feature_downloader/src/main/res/drawable/ic_delete.xml @@ -4,5 +4,5 @@ android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + \ No newline at end of file diff --git a/feature_downloader/src/main/res/drawable/ic_file_music.xml b/feature_downloader/src/main/res/drawable/ic_file_music.xml index 986cc4d5..a08cca0c 100644 --- a/feature_downloader/src/main/res/drawable/ic_file_music.xml +++ b/feature_downloader/src/main/res/drawable/ic_file_music.xml @@ -4,5 +4,5 @@ android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + \ No newline at end of file diff --git a/feature_downloader/src/main/res/layout/activity_download_management.xml b/feature_downloader/src/main/res/layout/activity_download_management.xml index c5eb111e..50c390ac 100644 --- a/feature_downloader/src/main/res/layout/activity_download_management.xml +++ b/feature_downloader/src/main/res/layout/activity_download_management.xml @@ -2,7 +2,6 @@ diff --git a/feature_downloader/src/main/res/layout/layout_empty_content.xml b/feature_downloader/src/main/res/layout/layout_empty_content.xml index 24a8ad8f..dcad6dbe 100644 --- a/feature_downloader/src/main/res/layout/layout_empty_content.xml +++ b/feature_downloader/src/main/res/layout/layout_empty_content.xml @@ -20,7 +20,7 @@ android:text="@string/emptyView_download_message" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:layout_width="wrap_content" - android:textColor="@color/colorItem" + android:textColor="@color/colorPrimary" android:layout_marginBottom="@dimen/margin_standard" android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/feature_downloader/src/main/res/layout/recycler_download_item.xml b/feature_downloader/src/main/res/layout/recycler_download_item.xml index e5964b91..eb37bdb1 100644 --- a/feature_downloader/src/main/res/layout/recycler_download_item.xml +++ b/feature_downloader/src/main/res/layout/recycler_download_item.xml @@ -15,7 +15,8 @@ android:id="@+id/textView_download_file_name" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:layout_width="0dp" - android:textColor="@color/colorItem" + android:textColor="@color/colorPrimary" + android:textStyle="bold" android:layout_weight="1" android:layout_height="wrap_content" /> @@ -48,6 +49,7 @@ style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="0dp" android:paddingEnd="@dimen/padding_standard" + android:paddingStart="@dimen/standard_padding_min" android:layout_weight="3" android:layout_gravity="center" android:layout_height="wrap_content" /> diff --git a/feature_listen_later_ui/src/main/java/com/allsoftdroid/audiobook/feature_listen_later_ui/utils/BindingUtils.kt b/feature_listen_later_ui/src/main/java/com/allsoftdroid/audiobook/feature_listen_later_ui/utils/BindingUtils.kt index 24bfba9d..8edfbdd6 100644 --- a/feature_listen_later_ui/src/main/java/com/allsoftdroid/audiobook/feature_listen_later_ui/utils/BindingUtils.kt +++ b/feature_listen_later_ui/src/main/java/com/allsoftdroid/audiobook/feature_listen_later_ui/utils/BindingUtils.kt @@ -7,6 +7,7 @@ import com.allsoftdroid.audiobook.feature_listen_later_ui.R import com.allsoftdroid.audiobook.feature_listen_later_ui.data.model.ListenLaterItemDomainModel import com.allsoftdroid.common.base.extension.CreateImageOverlay import com.allsoftdroid.common.base.network.ArchiveUtils +import com.allsoftdroid.common.base.utils.BindingUtils.getNormalizedText import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions @@ -63,12 +64,4 @@ fun TextView.setBookDuration(item: ListenLaterItemDomainModel?){ item?.let { text = it.duration } -} - -private fun getNormalizedText(text:String?,limit:Int):String{ - if(text?.length?:0>limit){ - return text?.substring(0,limit-3)+"..." - } - - return text?:"" } \ No newline at end of file diff --git a/feature_listen_later_ui/src/main/res/layout/fragment_listen_later_layout.xml b/feature_listen_later_ui/src/main/res/layout/fragment_listen_later_layout.xml index 52f02260..39e2b656 100644 --- a/feature_listen_later_ui/src/main/res/layout/fragment_listen_later_layout.xml +++ b/feature_listen_later_ui/src/main/res/layout/fragment_listen_later_layout.xml @@ -17,7 +17,7 @@ android:layout_height="?actionBarSize" app:layout_constraintEnd_toEndOf="parent" android:background="@color/black" - android:padding="2dp" + android:padding="@dimen/padding_min" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" android:id="@+id/toolbar" @@ -25,11 +25,11 @@ @@ -38,8 +38,9 @@ android:id="@+id/toolbar_title" android:textColor="@color/white" android:layout_width="0dp" + android:layout_marginStart="@dimen/margin_normal" android:textAppearance="@style/TextAppearance.AppCompat.Large" - android:gravity="center" + android:gravity="start|center_vertical" android:text="@string/toolbar_title_text" android:layout_height="match_parent" android:layout_weight="1"/> @@ -47,7 +48,7 @@ diff --git a/feature_listen_later_ui/src/main/res/values/dimens.xml b/feature_listen_later_ui/src/main/res/values/dimens.xml index 45d56c8b..ec0aea26 100644 --- a/feature_listen_later_ui/src/main/res/values/dimens.xml +++ b/feature_listen_later_ui/src/main/res/values/dimens.xml @@ -3,4 +3,8 @@ 24dp 16dp 90dp + + 2dp + 24dp + 16dp \ No newline at end of file diff --git a/feature_mini_player/src/main/java/com/allsoftdroid/audiobook/feature_mini_player/presentation/MiniPlayerFragment.kt b/feature_mini_player/src/main/java/com/allsoftdroid/audiobook/feature_mini_player/presentation/MiniPlayerFragment.kt index 7b6dca27..e2ee040c 100644 --- a/feature_mini_player/src/main/java/com/allsoftdroid/audiobook/feature_mini_player/presentation/MiniPlayerFragment.kt +++ b/feature_mini_player/src/main/java/com/allsoftdroid/audiobook/feature_mini_player/presentation/MiniPlayerFragment.kt @@ -47,6 +47,5 @@ class MiniPlayerFragment : BaseContainerFragment() { override fun onDestroy() { super.onDestroy() - FeatureMiniPlayerModule.unloadModule() } } \ No newline at end of file diff --git a/feature_mybooks/build.gradle b/feature_mybooks/build.gradle index 11e0c6c6..460a1bed 100644 --- a/feature_mybooks/build.gradle +++ b/feature_mybooks/build.gradle @@ -43,8 +43,10 @@ android { dependencies { implementation(project(path: ModuleDependency.LIBRARY_COMMON)) + implementation(project(path: ModuleDependency.DATABASE)) implementation(LibraryDependency.GLIDE) kapt(LibraryDependency.GLIDE_COMPILER) implementation(LibraryDependency.KOIN_X_VIEWMODEL) + implementation(LibraryDependency.LOTTIE) } diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/MyBooksFragment.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/MyBooksFragment.kt deleted file mode 100644 index ff57539c..00000000 --- a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/MyBooksFragment.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.allsoftdroid.audiobook.feature_mybooks - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback -import com.allsoftdroid.audiobook.feature_mybooks.databinding.FragmentMybooksLayoutBinding -import com.allsoftdroid.common.base.fragment.BaseUIFragment - -class MyBooksFragment : BaseUIFragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val dataBinding:FragmentMybooksLayoutBinding = inflateLayout(inflater, - R.layout.fragment_mybooks_layout,container,false) - - return dataBinding.root - } - - override fun handleBackPressEvent(callback: OnBackPressedCallback) { - callback.isEnabled = false - requireActivity().onBackPressed() - } -} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/model/BookMetadata.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/model/BookMetadata.kt new file mode 100644 index 00000000..89b7d779 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/model/BookMetadata.kt @@ -0,0 +1,7 @@ +package com.allsoftdroid.audiobook.feature_mybooks.data.model + +data class BookMetadata( + val title:String, + val author:String, + val totalTracks:Int +) \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/model/LocalBookDomainModel.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/model/LocalBookDomainModel.kt new file mode 100644 index 00000000..fe482516 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/model/LocalBookDomainModel.kt @@ -0,0 +1,10 @@ +package com.allsoftdroid.audiobook.feature_mybooks.data.model + +data class LocalBookDomainModel ( + val bookTitle:String, + val bookIdentifier:String, + val bookAuthor:String, + val bookChaptersDownloaded:Int, + val totalChapters:Int, + val fileNames:List +) \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/model/LocalBookFiles.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/model/LocalBookFiles.kt new file mode 100644 index 00000000..4a7b4cd3 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/model/LocalBookFiles.kt @@ -0,0 +1,6 @@ +package com.allsoftdroid.audiobook.feature_mybooks.data.model + +data class LocalBookFiles( + val identifier:String, + val filePath:List +) \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/repository/BookMetadataRepositoryImpl.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/repository/BookMetadataRepositoryImpl.kt new file mode 100644 index 00000000..44b58c3c --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/repository/BookMetadataRepositoryImpl.kt @@ -0,0 +1,27 @@ +package com.allsoftdroid.audiobook.feature_mybooks.data.repository + +import com.allsoftdroid.audiobook.feature_mybooks.data.model.BookMetadata +import com.allsoftdroid.audiobook.feature_mybooks.domain.IBookMetadataRepository +import com.allsoftdroid.database.metadataCacheDB.MetadataDao +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class BookMetadataRepositoryImpl( + private val metadataDao: MetadataDao, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +):IBookMetadataRepository { + override suspend fun getBookMetadata(identifier: String): BookMetadata { + + return withContext(dispatcher){ + val metadata = metadataDao.getMetadataNonLive(identifier) + val tracks = metadataDao.getTrackDetailsNonLive(metadata_id = identifier,formatContains = "64") + + if(metadata==null || tracks.isEmpty()) { + return@withContext BookMetadata(title ="",author = "",totalTracks =0) + }else{ + return@withContext BookMetadata(title =metadata.title,author = metadata.creator,totalTracks = tracks.size) + } + } + } +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/repository/LocalBooksRepositoryImpl.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/repository/LocalBooksRepositoryImpl.kt new file mode 100644 index 00000000..cd31d443 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/data/repository/LocalBooksRepositoryImpl.kt @@ -0,0 +1,101 @@ +package com.allsoftdroid.audiobook.feature_mybooks.data.repository + +import android.app.Application +import android.os.Environment +import com.allsoftdroid.audiobook.feature_mybooks.data.model.LocalBookFiles +import com.allsoftdroid.audiobook.feature_mybooks.domain.ILocalBooksRepository +import com.allsoftdroid.common.base.network.ArchiveUtils +import com.allsoftdroid.common.base.utils.LocalFilesForBook +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File + +class LocalBooksRepositoryImpl( + private val dispatcher: CoroutineDispatcher=Dispatchers.IO, + private val application: Application, + private val localFilesForBook:LocalFilesForBook +) : ILocalBooksRepository { + + override suspend fun getLocalBookFiles(): List = + withContext(dispatcher){ + val localBookFiles = mutableListOf() + + val rootFolder = ArchiveUtils.getDownloadsRootFolder(application) + Timber.d("Root Folder is $rootFolder") + + val directory = Environment.getExternalStoragePublicDirectory("$rootFolder/AudioBooks/") + val bookIds = directory.listFiles()?.map { + it.absolutePath.split("/").last() + } + + Timber.d("BookIds are $bookIds") + + bookIds?.map {bookId-> + val files = localFilesForBook.getDownloadedFilesList(bookId) + if(files.isNullOrEmpty()){ + localBookFiles.add(LocalBookFiles(identifier = bookId,filePath = emptyList())) + }else{ + Timber.i("$bookId : Found ${files.size} files") + localBookFiles.add(LocalBookFiles(identifier = bookId,filePath = files)) + } + } + + return@withContext localBookFiles + } + + override suspend fun removeBook(identifier: String) { + withContext(dispatcher){ + val rootFolder = ArchiveUtils.getDownloadsRootFolder(application) + Timber.d("Root Folder is $rootFolder") + + val directory = Environment.getExternalStoragePublicDirectory("$rootFolder/AudioBooks/") + val books = directory.listFiles()?.filter { + it.absolutePath.split("/").last() == identifier + } + + Timber.d("Books to be removed are : $books") + + books?.let { + it.forEach { folder -> + try { + deleteRecursive(folder) + }catch (e:Exception){ + Timber.e("Error: can't remove $folder") + } + } + } + } + } + + override suspend fun deleteAllChapters(identifier: String) { + withContext(dispatcher){ + val removeFiles = localFilesForBook.getDownloadedFilesList(identifier) + + removeFiles?.let {filePaths -> + Timber.d("Files to be removed are: $filePaths") + + filePaths.forEach { + try { + val file = File(it) + deleteRecursive(file) + }catch (e:Exception){ + Timber.e("Error: can't remove $it") + } + } + } + } + } + + private fun deleteRecursive(fileOrDirectory: File){ + + if(fileOrDirectory.isDirectory){ + fileOrDirectory.listFiles()?.forEach { + deleteRecursive(it) + } + } + + fileOrDirectory.delete() + } +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/di/LocalBooksModule.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/di/LocalBooksModule.kt new file mode 100644 index 00000000..1c05728d --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/di/LocalBooksModule.kt @@ -0,0 +1,96 @@ +package com.allsoftdroid.audiobook.feature_mybooks.di + +import com.allsoftdroid.audiobook.feature_mybooks.data.repository.BookMetadataRepositoryImpl +import com.allsoftdroid.audiobook.feature_mybooks.data.repository.LocalBooksRepositoryImpl +import com.allsoftdroid.audiobook.feature_mybooks.domain.IBookMetadataRepository +import com.allsoftdroid.audiobook.feature_mybooks.domain.ILocalBooksRepository +import com.allsoftdroid.audiobook.feature_mybooks.domain.LocalBookListUsecase +import com.allsoftdroid.audiobook.feature_mybooks.presentation.LocalBooksViewModel +import com.allsoftdroid.database.common.AudioBookDatabase +import com.allsoftdroid.database.metadataCacheDB.MetadataDao +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.context.loadKoinModules +import org.koin.core.context.unloadKoinModules +import org.koin.core.module.Module +import org.koin.core.qualifier.named +import org.koin.dsl.module +import kotlin.coroutines.CoroutineContext + +object LocalBooksModule { + + fun injectFeature() = + loadFeature + + fun unLoadModules(){ + unloadKoinModules( + listOf( + localBooksViewModel, + dataModule, + jobModule + ) + ) + } + + private val loadFeature by lazy { + loadKoinModules(listOf( + localBooksViewModel, + usecaseModule, + dataModule, + jobModule + )) + } + + var localBooksViewModel : Module = module { + viewModel { + LocalBooksViewModel( + bookListUsecase = get() + ) + } + } + + var usecaseModule : Module = module { + factory { + LocalBookListUsecase( + localBooksRepository = get(), + bookMetadataRepository = get() + ) + } + } + + var dataModule : Module = module { + factory { + LocalBooksRepositoryImpl( + application = get(), + localFilesForBook = get() + ) as ILocalBooksRepository + } + + factory { + BookMetadataRepositoryImpl( + metadataDao = get(named(name = BEAN_NAME)) + ) as IBookMetadataRepository + } + + single(named(name = BEAN_NAME)) { + AudioBookDatabase.getDatabase(get()).metadataDao() as MetadataDao + } + } + + var jobModule : Module = module { + + single(named(name = SUPER_VISOR_JOB)) { + SupervisorJob() + } + + factory(named(name = VIEW_MODEL_SCOPE)) { + CoroutineScope(get(named(name = SUPER_VISOR_JOB)) as CoroutineContext + Dispatchers.Main) + } + } + + const val SUPER_VISOR_JOB = "SuperVisorJob_LocalBooks" + const val VIEW_MODEL_SCOPE = "ViewModelScope_LocalBooks" + private const val BEAN_NAME = "LocalBooksFragment" +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/domain/IBookMetadataRepository.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/domain/IBookMetadataRepository.kt new file mode 100644 index 00000000..4c88d6d7 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/domain/IBookMetadataRepository.kt @@ -0,0 +1,7 @@ +package com.allsoftdroid.audiobook.feature_mybooks.domain + +import com.allsoftdroid.audiobook.feature_mybooks.data.model.BookMetadata + +interface IBookMetadataRepository { + suspend fun getBookMetadata(identifier:String):BookMetadata +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/domain/ILocalBooksRepository.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/domain/ILocalBooksRepository.kt new file mode 100644 index 00000000..b5c50031 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/domain/ILocalBooksRepository.kt @@ -0,0 +1,11 @@ +package com.allsoftdroid.audiobook.feature_mybooks.domain + +import com.allsoftdroid.audiobook.feature_mybooks.data.model.LocalBookFiles + +interface ILocalBooksRepository { + suspend fun getLocalBookFiles():List + + suspend fun removeBook(identifier:String) + + suspend fun deleteAllChapters(identifier: String) +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/domain/LocalBookListUsecase.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/domain/LocalBookListUsecase.kt new file mode 100644 index 00000000..4e447119 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/domain/LocalBookListUsecase.kt @@ -0,0 +1,49 @@ +package com.allsoftdroid.audiobook.feature_mybooks.domain + +import com.allsoftdroid.audiobook.feature_mybooks.data.model.LocalBookDomainModel +import timber.log.Timber + +class LocalBookListUsecase( + private val localBooksRepository: ILocalBooksRepository, + private val bookMetadataRepository: IBookMetadataRepository +) { + + suspend fun getBookList():List{ + + val localBooks = mutableListOf() + + val localFiles = localBooksRepository.getLocalBookFiles() + Timber.d("local files size is ${localFiles.size}") + + for (file in localFiles){ + + val metadata = bookMetadataRepository.getBookMetadata(file.identifier) + + Timber.d("File Identifier is : ${file.identifier}") + Timber.d("Metadata is : ${metadata.title}") + Timber.d("File is : ${file.filePath}") + + if (metadata.title.isNotEmpty()){ + localBooks.add( + LocalBookDomainModel( + bookTitle = metadata.title, + bookIdentifier = file.identifier, + bookAuthor = metadata.author, + bookChaptersDownloaded = file.filePath.size, + totalChapters = metadata.totalTracks, + fileNames = file.filePath + )) + } + } + + return localBooks + } + + suspend fun removeBook(bookId:String){ + localBooksRepository.removeBook(bookId) + } + + suspend fun removeChapters(bookId: String){ + localBooksRepository.deleteAllChapters(bookId) + } +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/LocalBooksViewModel.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/LocalBooksViewModel.kt new file mode 100644 index 00000000..a14d6e46 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/LocalBooksViewModel.kt @@ -0,0 +1,89 @@ +package com.allsoftdroid.audiobook.feature_mybooks.presentation + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.allsoftdroid.audiobook.feature_mybooks.data.model.LocalBookDomainModel +import com.allsoftdroid.audiobook.feature_mybooks.di.LocalBooksModule.SUPER_VISOR_JOB +import com.allsoftdroid.audiobook.feature_mybooks.di.LocalBooksModule.VIEW_MODEL_SCOPE +import com.allsoftdroid.audiobook.feature_mybooks.domain.LocalBookListUsecase +import com.allsoftdroid.audiobook.feature_mybooks.utils.Empty +import com.allsoftdroid.audiobook.feature_mybooks.utils.RequestStatus +import com.allsoftdroid.audiobook.feature_mybooks.utils.Started +import com.allsoftdroid.audiobook.feature_mybooks.utils.Success +import com.allsoftdroid.common.base.extension.Event +import kotlinx.coroutines.CompletableJob +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.koin.core.KoinComponent +import org.koin.core.inject +import org.koin.core.qualifier.named +import timber.log.Timber + +class LocalBooksViewModel( + private val bookListUsecase: LocalBookListUsecase +) : ViewModel(),KoinComponent { + /** + * cancelling this job cancels all the job started by this viewmodel + */ + private val viewModelJob: CompletableJob by inject(named(name = SUPER_VISOR_JOB)) + + /** + * main scope for all coroutine launched by viewmodel + */ + private val viewModelScope : CoroutineScope by inject(named(name = VIEW_MODEL_SCOPE)) + + private var _books:List? = null + + private var _requestStatus = MutableLiveData>() + val requestStatus : LiveData> = _requestStatus + + + private fun loadBooks(){ + viewModelScope.launch { + Timber.d("sending started response") + _requestStatus.value = Event(Started) + + val books = bookListUsecase.getBookList() + if(books.isEmpty()){ + Timber.d("books is empty sending empty response") + _requestStatus.value = Event(Empty) + }else{ + Timber.d("books is not empty sending response") + _requestStatus.value = Event(Success(books)) + _books = books + } + } + } + + fun loadFromCacheOrReload(){ + + val books = _books + + if (books.isNullOrEmpty()){ + loadBooks() + }else{ + _requestStatus.value = Event(Success(books)) + } + } + + fun removeBook(identifier:String){ + viewModelScope.launch { + bookListUsecase.removeBook(identifier) + loadBooks() + } + } + + fun removeAllChapters(identifier: String){ + viewModelScope.launch { + bookListUsecase.removeChapters(identifier) + loadBooks() + } + } + + //cancel the job when viewmodel is not longer in use + override fun onCleared() { + super.onCleared() + viewModelJob.cancel() + } +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/MyBooksFragment.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/MyBooksFragment.kt new file mode 100644 index 00000000..71b01d1b --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/MyBooksFragment.kt @@ -0,0 +1,123 @@ +package com.allsoftdroid.audiobook.feature_mybooks.presentation + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.core.os.bundleOf +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.allsoftdroid.audiobook.feature_mybooks.R +import com.allsoftdroid.audiobook.feature_mybooks.databinding.FragmentMybooksLayoutBinding +import com.allsoftdroid.audiobook.feature_mybooks.di.LocalBooksModule +import com.allsoftdroid.audiobook.feature_mybooks.presentation.recyclerView.ItemClickedListener +import com.allsoftdroid.audiobook.feature_mybooks.presentation.recyclerView.LocalBookAdapter +import com.allsoftdroid.audiobook.feature_mybooks.presentation.recyclerView.OptionsClickedListener +import com.allsoftdroid.audiobook.feature_mybooks.utils.Empty +import com.allsoftdroid.audiobook.feature_mybooks.utils.Started +import com.allsoftdroid.audiobook.feature_mybooks.utils.Success +import com.allsoftdroid.common.base.fragment.BaseUIFragment +import org.koin.core.KoinComponent +import org.koin.core.inject +import timber.log.Timber + +class MyBooksFragment : BaseUIFragment(),KoinComponent { + + private val localBooksViewModel : LocalBooksViewModel by inject() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val dataBinding:FragmentMybooksLayoutBinding = inflateLayout(inflater, + R.layout.fragment_mybooks_layout,container,false) + + LocalBooksModule.injectFeature() + + dataBinding.lifecycleOwner = viewLifecycleOwner + + val adapter = LocalBookAdapter( + this.requireActivity(), + + ItemClickedListener {bookId-> + //Navigate to display page + val bundle = bundleOf("bookId" to bookId) + + this.findNavController() + .navigate(R.id.action_MyBooksFragment_to_AudioBookDetailsFragment,bundle) + }, + + OptionsClickedListener( + onDeleteBook = { + localBooksViewModel.removeBook(it.bookIdentifier) + }, + + onRemoveChapters = { + localBooksViewModel.removeAllChapters(it.bookIdentifier) + } + ) + ) + + dataBinding.recyclerViewBooks.adapter = adapter + + //recycler view layout manager + dataBinding.recyclerViewBooks.apply { + layoutManager = LinearLayoutManager(context) + } + + dataBinding.toolbarBackArrow.setOnClickListener { + onBackPressed() + } + + localBooksViewModel.requestStatus.observe(viewLifecycleOwner, Observer { + it.getContentIfNotHandled()?.let { status-> + when(status){ + is Empty -> { + Timber.d("Empty result") + dataBinding.loadingProgressbar.visibility = View.GONE + dataBinding.noLocalBooks.visibility = View.VISIBLE + dataBinding.bookCount.visibility = View.GONE + dataBinding.recyclerViewBooks.visibility = View.GONE + } + + is Started -> { + Timber.d("Started the request") + dataBinding.loadingProgressbar.visibility = View.VISIBLE + dataBinding.noLocalBooks.visibility = View.GONE + dataBinding.bookCount.visibility = View.GONE + dataBinding.recyclerViewBooks.visibility = View.GONE + } + + is Success -> { + Timber.d("Received result:${status.list}") + dataBinding.loadingProgressbar.visibility = View.GONE + dataBinding.noLocalBooks.visibility = View.GONE + + adapter.submitList(status.list) + dataBinding.recyclerViewBooks.visibility = View.VISIBLE + + dataBinding.bookCount.apply { + visibility = View.VISIBLE + text = requireActivity().getString(R.string.books_in_storage,status.list.size) + } + } + } + } + }) + + return dataBinding.root + } + + override fun handleBackPressEvent(callback: OnBackPressedCallback) { + callback.isEnabled = false + requireActivity().onBackPressed() + } + + override fun onResume() { + super.onResume() + localBooksViewModel.loadFromCacheOrReload() + } +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/recyclerView/LocalBookAdapter.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/recyclerView/LocalBookAdapter.kt new file mode 100644 index 00000000..a7ba0198 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/recyclerView/LocalBookAdapter.kt @@ -0,0 +1,108 @@ +package com.allsoftdroid.audiobook.feature_mybooks.presentation.recyclerView + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.allsoftdroid.audiobook.feature_mybooks.R +import com.allsoftdroid.audiobook.feature_mybooks.data.model.LocalBookDomainModel +import timber.log.Timber + +class LocalBookAdapter( + private val context: Context, + private val itemClickedListener: ItemClickedListener, + private val optionsClickedListener: OptionsClickedListener +): ListAdapter(RandomBookDiffCallback()) { + + /** + * Create view Holder of type BookViewHolder + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return LocalBookItemViewHolder.from(parent) + } + + /** + * Bind the ViewHolder with the data item + */ + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when(holder){ + is LocalBookItemViewHolder ->{ + val dataItem = getItem(position) + holder.bind(dataItem,itemClickedListener) + + holder.buttonViewOptions.setOnClickListener { + showPopupMenu(dataItem,it) + } + } + + else -> throw Exception("View Holder type is unknown:$holder") + } + } + + private fun showPopupMenu(localBook: LocalBookDomainModel,view: View) { + val popUp = PopupMenu(context,view) + popUp.inflate(R.menu.local_books_option_menu) + + popUp.setOnMenuItemClickListener { + when (it.itemId){ + R.id.ItemOptions_removeAllChapters -> { + optionsClickedListener.onRemoveChaptersClicked(localBook) + } + + R.id.ItemOptions_removeBook -> { + + optionsClickedListener.onDeleteBookClicked(localBook) + Timber.d("remove clicked for ${localBook.bookTitle}") + } + } + + return@setOnMenuItemClickListener false + } + popUp.show() + } +} + + +/** +class to smartly check for difference in new loaded list and old list +It enhance the performance of the recycler view + */ +class RandomBookDiffCallback : DiffUtil.ItemCallback(){ + /** + * Compare items based on identifier fields + */ + override fun areItemsTheSame(oldItem: LocalBookDomainModel, newItem: LocalBookDomainModel): Boolean { + return oldItem.bookIdentifier==newItem.bookIdentifier + } + + /** + * Check every fields to verify for same content. + */ + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: LocalBookDomainModel, newItem: LocalBookDomainModel): Boolean { + /* + Since book is data class so here all the fields are automatically checked + */ + return oldItem == newItem + } + +} + +/* +listener to check for the click event + */ +class ItemClickedListener(val clickListener : (identifier : String)->Unit){ + fun onItemClicked(listenLater : LocalBookDomainModel) = clickListener(listenLater.bookIdentifier) +} + +class OptionsClickedListener( + val onDeleteBook : (identifier : LocalBookDomainModel)->Unit, + val onRemoveChapters : (identifier : LocalBookDomainModel)->Unit) +{ + fun onDeleteBookClicked(listenLater : LocalBookDomainModel) = onDeleteBook(listenLater) + fun onRemoveChaptersClicked(listenLater : LocalBookDomainModel) = onRemoveChapters(listenLater) +} diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/recyclerView/LocalBookItemViewHolder.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/recyclerView/LocalBookItemViewHolder.kt new file mode 100644 index 00000000..a0b93525 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/presentation/recyclerView/LocalBookItemViewHolder.kt @@ -0,0 +1,31 @@ +package com.allsoftdroid.audiobook.feature_mybooks.presentation.recyclerView + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.allsoftdroid.audiobook.feature_mybooks.data.model.LocalBookDomainModel +import com.allsoftdroid.audiobook.feature_mybooks.databinding.MyBooksItemLayoutBinding + +class LocalBookItemViewHolder private constructor(private val binding : MyBooksItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) { + + lateinit var buttonViewOptions: View + + // bind the data to the view + fun bind(item: LocalBookDomainModel, itemClickedListener: ItemClickedListener) { + binding.book = item + binding.clickListener = itemClickedListener + buttonViewOptions = binding.ItemOptions + binding.executePendingBindings() + } + + //construct the viewholder + companion object { + fun from(parent: ViewGroup): LocalBookItemViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = MyBooksItemLayoutBinding.inflate(layoutInflater, parent, false) + + return LocalBookItemViewHolder(binding) + } + } +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/utils/BindingUtil.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/utils/BindingUtil.kt new file mode 100644 index 00000000..7a7ea5e5 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/utils/BindingUtil.kt @@ -0,0 +1,65 @@ +package com.allsoftdroid.audiobook.feature_mybooks.utils + +import android.widget.ImageView +import android.widget.TextView +import androidx.databinding.BindingAdapter +import com.allsoftdroid.audiobook.feature_mybooks.R +import com.allsoftdroid.audiobook.feature_mybooks.data.model.LocalBookDomainModel +import com.allsoftdroid.common.base.extension.CreateImageOverlay +import com.allsoftdroid.common.base.network.ArchiveUtils +import com.allsoftdroid.common.base.utils.BindingUtils.getNormalizedText +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions + +@BindingAdapter("bookImage") +fun setImageUrl(imageView: ImageView, item: LocalBookDomainModel?) { + + item?.let { + val url = ArchiveUtils.getThumbnail(item.bookIdentifier) + + Glide + .with(imageView.context) + .asBitmap() + .load(url) + .override(250,250) + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) + .dontAnimate() + .apply( + RequestOptions() + .placeholder(R.drawable.loading_animation) + .error( + CreateImageOverlay + .with(imageView.context) + .buildOverlay(front = R.drawable.ic_book_play,back = R.drawable.gradiant_background) + ) + ) + .into(imageView) + } +} + + +/* +Binding adapter for updating the title in list items + */ +@BindingAdapter("bookTitle") +fun TextView.setBookTitle(item: LocalBookDomainModel?){ + item?.let { + text = getNormalizedText(item.bookTitle, 30) + } +} + +@BindingAdapter("bookAuthor") +fun TextView.setBookAuthor(item: LocalBookDomainModel?){ + item?.let { + text = + getNormalizedText(item.bookAuthor, 30) + } +} + +@BindingAdapter("bookChapterCount") +fun TextView.setBookDuration(item: LocalBookDomainModel?){ + item?.let { + text = context.getString(R.string.chapters_label,it.bookChaptersDownloaded,it.totalChapters) + } +} \ No newline at end of file diff --git a/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/utils/RequestStatus.kt b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/utils/RequestStatus.kt new file mode 100644 index 00000000..f8deb683 --- /dev/null +++ b/feature_mybooks/src/main/java/com/allsoftdroid/audiobook/feature_mybooks/utils/RequestStatus.kt @@ -0,0 +1,9 @@ +package com.allsoftdroid.audiobook.feature_mybooks.utils + +import com.allsoftdroid.audiobook.feature_mybooks.data.model.LocalBookDomainModel + +sealed class RequestStatus + +data class Success(val list : List) : RequestStatus() +object Empty : RequestStatus() +object Started : RequestStatus() diff --git a/feature_mybooks/src/main/res/drawable/background_round_corner_dark_border.xml b/feature_mybooks/src/main/res/drawable/background_round_corner_dark_border.xml new file mode 100644 index 00000000..e0826a52 --- /dev/null +++ b/feature_mybooks/src/main/res/drawable/background_round_corner_dark_border.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/feature_mybooks/src/main/res/layout/fragment_mybooks_layout.xml b/feature_mybooks/src/main/res/layout/fragment_mybooks_layout.xml index 73bd022a..6e627321 100644 --- a/feature_mybooks/src/main/res/layout/fragment_mybooks_layout.xml +++ b/feature_mybooks/src/main/res/layout/fragment_mybooks_layout.xml @@ -1,17 +1,94 @@ - + - + + + + + + + + + + + android:layout_height="wrap_content" + android:clipToPadding="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/bookCount"/> + + + + + + - \ No newline at end of file diff --git a/feature_mybooks/src/main/res/layout/layout_no_books_found_local_storage.xml b/feature_mybooks/src/main/res/layout/layout_no_books_found_local_storage.xml new file mode 100644 index 00000000..b8e1e814 --- /dev/null +++ b/feature_mybooks/src/main/res/layout/layout_no_books_found_local_storage.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/feature_mybooks/src/main/res/layout/my_books_item_layout.xml b/feature_mybooks/src/main/res/layout/my_books_item_layout.xml new file mode 100644 index 00000000..8a57d1c4 --- /dev/null +++ b/feature_mybooks/src/main/res/layout/my_books_item_layout.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature_mybooks/src/main/res/menu/local_books_option_menu.xml b/feature_mybooks/src/main/res/menu/local_books_option_menu.xml new file mode 100644 index 00000000..87a7ffe3 --- /dev/null +++ b/feature_mybooks/src/main/res/menu/local_books_option_menu.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/feature_mybooks/src/main/res/values/dimens.xml b/feature_mybooks/src/main/res/values/dimens.xml new file mode 100644 index 00000000..8d3c3561 --- /dev/null +++ b/feature_mybooks/src/main/res/values/dimens.xml @@ -0,0 +1,18 @@ + + + 4dp + 16dp + 24dp + + 2dp + 8dp + + 16dp + 16dp + 8dp + + 90dp + + 16dp + 24dp + \ No newline at end of file diff --git a/feature_mybooks/src/main/res/values/strings.xml b/feature_mybooks/src/main/res/values/strings.xml new file mode 100644 index 00000000..abf3be0c --- /dev/null +++ b/feature_mybooks/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + My Books + navigate to previous screen + No Books found in Storage + book thumbnail image + + Remove All Chapters + Delete Book + Number of chapters available locally + %1d Books in Storage + %d/%d Chapters + \ No newline at end of file