diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..cd7974c
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LOCALIZATION_README.md b/LOCALIZATION_README.md
new file mode 100644
index 0000000..f330f5f
--- /dev/null
+++ b/LOCALIZATION_README.md
@@ -0,0 +1,175 @@
+# Multi-Language Localization Setup
+
+This project now supports multiple languages: English, Portuguese, and Spanish.
+
+## 📁 File Structure
+
+```
+app/src/main/res/
+├── values/
+│ └── strings.xml # English strings (default)
+├── values-pt/
+│ └── strings.xml # Portuguese strings
+└── values-es/
+ └── strings.xml # Spanish strings
+```
+
+## 🌐 How It Works
+
+### Automatic Language Detection
+- The app automatically detects the device's language setting
+- If the device is set to Portuguese, it will use `values-pt/strings.xml`
+- If the device is set to Spanish, it will use `values-es/strings.xml`
+- If the device is set to any other language, it will use `values/strings.xml` (English)
+
+### String Resources
+All user-facing text in the app is now localized:
+- **UI Labels**: Buttons, titles, descriptions
+- **Dialog Messages**: Confirmations, errors, information
+- **Content Descriptions**: Accessibility labels for screen readers
+- **Settings**: Configuration options and descriptions
+
+## 🔧 Implementation Details
+
+### Compose UI
+The app uses `stringResource()` calls throughout the Compose UI:
+```kotlin
+Text(stringResource(R.string.app_name))
+Text(stringResource(R.string.settings))
+```
+
+### Android Manifest
+The app name is localized:
+```xml
+android:label="@string/app_name"
+```
+
+## 📝 Adding New Strings
+
+### 1. Add to English (values/strings.xml)
+```xml
+New Feature
+```
+
+### 2. Add to Portuguese (values-pt/strings.xml)
+```xml
+Nova Funcionalidade
+```
+
+### 3. Add to Spanish (values-es/strings.xml)
+```xml
+Nueva Funcionalidad
+```
+
+### 4. Use in Code
+```kotlin
+Text(stringResource(R.string.new_feature_title))
+```
+
+## 🌍 Supported Languages
+
+| Language | Code | File | Status |
+|----------|------|------|---------|
+| English | `en` | `values/strings.xml` | ✅ Complete |
+| Portuguese | `pt` | `values-pt/strings.xml` | ✅ Complete |
+| Spanish | `es` | `values-es/strings.xml` | ✅ Complete |
+
+## 🚀 Adding More Languages
+
+To add another language (e.g., French):
+
+1. Create `app/src/main/res/values-fr/strings.xml`
+2. Copy all strings from `values/strings.xml`
+3. Translate each string to French
+4. The app will automatically support French when the device language is set to French
+
+## 📱 Testing Localization
+
+### Method 1: Device Settings
+1. Go to Device Settings → System → Languages & input → Languages
+2. Add your desired language and move it to the top
+3. Restart the app
+4. All text should now appear in the selected language
+
+### Method 2: Android Studio
+1. Open Android Studio
+2. Go to Run → Edit Configurations
+3. In "Launch Options", set "Language" to your desired language code (e.g., "es" for Spanish)
+4. Run the app - it will launch in the selected language
+
+### Method 3: ADB Command
+```bash
+# For Spanish
+adb shell setprop persist.sys.language es
+adb shell setprop persist.sys.country ES
+
+# For Portuguese
+adb shell setprop persist.sys.language pt
+adb shell setprop persist.sys.country BR
+
+# For English
+adb shell setprop persist.sys.language en
+adb shell setprop persist.sys.country US
+```
+
+## 🔍 Common Issues
+
+### Missing Translations
+If a string is missing from a language file:
+- The app will fall back to English
+- Check the logcat for warnings about missing resources
+
+### String Format Issues
+Some strings might need special handling for different languages:
+- Date formats
+- Number formats
+- Pluralization rules
+- Gender-specific language variations
+
+### RTL Support
+All supported languages (English, Portuguese, Spanish) are left-to-right (LTR), so no special RTL handling is needed.
+
+## 📚 Best Practices
+
+1. **Never hardcode strings** - Always use `stringResource()`
+2. **Keep translations up to date** - Update all language files when adding new features
+3. **Test on real devices** - Emulator language switching can be unreliable
+4. **Use descriptive names** - String keys should be self-explanatory
+5. **Group related strings** - Use comments to organize strings by feature
+6. **Consider cultural differences** - Some phrases may need adaptation beyond direct translation
+
+## 🛠️ Maintenance
+
+### Regular Tasks
+- Review new strings added to English
+- Translate new strings to Portuguese and Spanish
+- Test all languages regularly
+- Update this documentation when adding new languages
+
+### Quality Assurance
+- Have native speakers review translations
+- Test on different Android versions
+- Verify accessibility with screen readers
+- Check for text overflow in different languages
+- Consider regional variations (e.g., European vs Latin American Spanish)
+
+## 🌎 Regional Considerations
+
+### Spanish
+- **Spain (es-ES)**: Uses "vosotros" form and European Spanish vocabulary
+- **Latin America (es-419)**: Uses "ustedes" form and Latin American vocabulary
+- Current implementation uses neutral Latin American Spanish
+
+### Portuguese
+- **Portugal (pt-PT)**: European Portuguese vocabulary and pronunciation
+- **Brazil (pt-BR)**: Brazilian Portuguese vocabulary and pronunciation
+- Current implementation uses Brazilian Portuguese
+
+## 📖 Resources
+
+- [Android Localization Guide](https://developer.android.com/guide/topics/resources/localization)
+- [Android String Resources](https://developer.android.com/guide/topics/resources/string-resource)
+- [Compose Localization](https://developer.android.com/jetpack/compose/localization)
+- [Material Design Localization](https://material.io/design/usability/bidirectionality.html)
+- [Spanish Language Resources](https://www.rae.es/)
+- [Portuguese Language Resources](https://www.priberam.pt/)
diff --git a/app/src/main/java/com/mobyle/abbay/data/datasource/local/books/BooksLocalDataSource.kt b/app/src/main/java/com/mobyle/abbay/data/datasource/local/books/BooksLocalDataSource.kt
index 6b7c770..9c50419 100644
--- a/app/src/main/java/com/mobyle/abbay/data/datasource/local/books/BooksLocalDataSource.kt
+++ b/app/src/main/java/com/mobyle/abbay/data/datasource/local/books/BooksLocalDataSource.kt
@@ -2,6 +2,7 @@ package com.mobyle.abbay.data.datasource.local.books
import com.mobyle.abbay.data.model.BookFileEntity
import com.mobyle.abbay.data.model.MultipleBooksEntity
+import com.model.Book
import kotlinx.coroutines.flow.Flow
interface BooksLocalDataSource {
@@ -9,13 +10,12 @@ interface BooksLocalDataSource {
fun observeBookFilesList(): Flow>
- suspend fun getBookFilesList(): List
+ suspend fun getBooksList(): List
- suspend fun addBookFileList(filesList: List)
-
- suspend fun getMultipleBooksList(): List
-
- suspend fun addMultipleBooksList(booksList: List)
+ suspend fun upsertBooksList(
+ singleFileBooksList: List,
+ multipleBooksList: List
+ )
suspend fun deleteBook(id: String)
@@ -31,7 +31,7 @@ interface BooksLocalDataSource {
fun getBooksFolder(): String?
- fun hasShownReloadGuide() : Boolean
+ fun hasShownReloadGuide(): Boolean
fun setReloadGuideAsShown()
diff --git a/app/src/main/java/com/mobyle/abbay/data/datasource/local/books/BooksLocalDataSourceImpl.kt b/app/src/main/java/com/mobyle/abbay/data/datasource/local/books/BooksLocalDataSourceImpl.kt
index 8c9befe..5e29c03 100644
--- a/app/src/main/java/com/mobyle/abbay/data/datasource/local/books/BooksLocalDataSourceImpl.kt
+++ b/app/src/main/java/com/mobyle/abbay/data/datasource/local/books/BooksLocalDataSourceImpl.kt
@@ -5,6 +5,7 @@ import com.mobyle.abbay.data.datasource.local.keystore.KeyValueStore
import com.mobyle.abbay.data.datasource.local.keystore.KeyValueStoreKeys
import com.mobyle.abbay.data.model.BookFileEntity
import com.mobyle.abbay.data.model.MultipleBooksEntity
+import com.model.Book
import javax.inject.Inject
class BooksLocalDataSourceImpl @Inject constructor(
@@ -16,16 +17,17 @@ class BooksLocalDataSourceImpl @Inject constructor(
override fun observeBookFilesList() = booksDao.observeBookFilesList()
- override suspend fun getBookFilesList(): List = booksDao.getBookFilesList()
-
- override suspend fun addBookFileList(filesList: List) =
- booksDao.insertBookFilesList(filesList)
-
- override suspend fun getMultipleBooksList(): List =
- booksDao.getMultipleBooksList()
+ override suspend fun upsertBooksList(
+ singleFileBooksList: List,
+ multipleBooksList: List
+ ) {
+ booksDao.upsertBooksList(
+ multipleBooksList = multipleBooksList,
+ singleFileBooksList = singleFileBooksList
+ )
+ }
- override suspend fun addMultipleBooksList(booksList: List) =
- booksDao.insertMultipleBooksList(booksList)
+ override suspend fun getBooksList(): List = booksDao.getBooksList()
override suspend fun deleteBook(id: String) {
booksDao.deleteBookFile(id)
@@ -36,8 +38,7 @@ class BooksLocalDataSourceImpl @Inject constructor(
}
override suspend fun clearBooks() {
- booksDao.deleteAllBookFiles()
- booksDao.deleteAllMultipleBooks()
+ booksDao.deleteAllBooks()
keyValueStore.deleteAllBookInformation()
}
diff --git a/app/src/main/java/com/mobyle/abbay/data/datasource/local/daos/BooksDao.kt b/app/src/main/java/com/mobyle/abbay/data/datasource/local/daos/BooksDao.kt
index 6768760..dcffe7a 100644
--- a/app/src/main/java/com/mobyle/abbay/data/datasource/local/daos/BooksDao.kt
+++ b/app/src/main/java/com/mobyle/abbay/data/datasource/local/daos/BooksDao.kt
@@ -4,8 +4,11 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
+import androidx.room.Transaction
+import com.mobyle.abbay.data.mappers.toDomain
import com.mobyle.abbay.data.model.BookFileEntity
import com.mobyle.abbay.data.model.MultipleBooksEntity
+import com.model.Book
import kotlinx.coroutines.flow.Flow
@Dao
@@ -45,4 +48,30 @@ interface BooksDao {
@Query("UPDATE MultipleBooksEntity SET progress = :progress, currentBookPosition = :currentPosition WHERE id = :id")
suspend fun updateMultipleBookProgress(id: String, currentPosition: Int, progress: Long)
+
+ @Transaction
+ suspend fun deleteAllBooks(
+ ) {
+ deleteAllBookFiles()
+ deleteAllMultipleBooks()
+ }
+
+ @Transaction
+ suspend fun upsertBooksList(
+ multipleBooksList: List,
+ singleFileBooksList: List
+ ) {
+ deleteAllBooks()
+ insertMultipleBooksList(multipleBooksList)
+ insertBookFilesList(singleFileBooksList)
+ }
+
+ @Transaction
+ suspend fun getBooksList(): List {
+ return getBookFilesList().map {
+ it.toDomain()
+ } + getMultipleBooksList().map {
+ it.toDomain()
+ }
+ }
}
diff --git a/app/src/main/java/com/mobyle/abbay/data/mappers/DomainToEntityMappers.kt b/app/src/main/java/com/mobyle/abbay/data/mappers/DomainToEntityMappers.kt
index 91989b7..1367ccd 100644
--- a/app/src/main/java/com/mobyle/abbay/data/mappers/DomainToEntityMappers.kt
+++ b/app/src/main/java/com/mobyle/abbay/data/mappers/DomainToEntityMappers.kt
@@ -1,8 +1,10 @@
package com.mobyle.abbay.data.mappers
import com.mobyle.abbay.data.model.BookFileEntity
+import com.mobyle.abbay.data.model.BookTypeEntity
import com.mobyle.abbay.data.model.MultipleBooksEntity
import com.model.BookFile
+import com.model.BookType
import com.model.MultipleBooks
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -20,7 +22,8 @@ fun MultipleBooks.toEntity(): MultipleBooksEntity = MultipleBooksEntity(
duration = duration,
currentBookPosition = currentBookPosition,
speed = speed,
- hasError = hasError
+ hasError = hasError,
+ type = type.toEntity()
)
fun BookFile.toEntity() = BookFileEntity(
@@ -31,7 +34,13 @@ fun BookFile.toEntity() = BookFileEntity(
progress = progress,
duration = duration,
speed = speed,
- hasError = hasError
+ hasError = hasError,
+ type = type.toEntity()
)
+private fun BookType.toEntity(): BookTypeEntity = when (this) {
+ BookType.FILE -> BookTypeEntity.FILE
+ else -> BookTypeEntity.FOLDER
+}
+
private fun BookFileEntity.toJson() = Json.encodeToString(this)
\ No newline at end of file
diff --git a/app/src/main/java/com/mobyle/abbay/data/mappers/EntityToDomainMappers.kt b/app/src/main/java/com/mobyle/abbay/data/mappers/EntityToDomainMappers.kt
index f419f22..62bc37f 100644
--- a/app/src/main/java/com/mobyle/abbay/data/mappers/EntityToDomainMappers.kt
+++ b/app/src/main/java/com/mobyle/abbay/data/mappers/EntityToDomainMappers.kt
@@ -1,8 +1,10 @@
package com.mobyle.abbay.data.mappers
import com.mobyle.abbay.data.model.BookFileEntity
+import com.mobyle.abbay.data.model.BookTypeEntity
import com.mobyle.abbay.data.model.MultipleBooksEntity
import com.model.BookFile
+import com.model.BookType
import com.model.MultipleBooks
import kotlinx.serialization.json.Json
@@ -18,7 +20,8 @@ fun MultipleBooksEntity.toDomain(): MultipleBooks = MultipleBooks(
duration = duration,
currentBookPosition = currentBookPosition,
speed = speed,
- hasError = hasError
+ hasError = hasError,
+ type = type.toDomain(),
)
fun BookFileEntity.toDomain() = BookFile(
@@ -29,7 +32,13 @@ fun BookFileEntity.toDomain() = BookFile(
progress = progress,
duration = duration,
speed = speed,
- hasError = hasError
+ hasError = hasError,
+ type = type.toDomain(),
)
+private fun BookTypeEntity.toDomain(): BookType = when(this) {
+ BookTypeEntity.FILE -> BookType.FILE
+ else -> BookType.FOLDER
+}
+
private fun String.toEntity() = Json.decodeFromString(this)
\ No newline at end of file
diff --git a/app/src/main/java/com/mobyle/abbay/data/model/BookFileEntity.kt b/app/src/main/java/com/mobyle/abbay/data/model/BookFileEntity.kt
index 1979369..dadebe1 100644
--- a/app/src/main/java/com/mobyle/abbay/data/model/BookFileEntity.kt
+++ b/app/src/main/java/com/mobyle/abbay/data/model/BookFileEntity.kt
@@ -14,5 +14,6 @@ class BookFileEntity(
val progress: Long,
val duration: Long,
val speed: Float,
- val hasError: Boolean
+ val hasError: Boolean,
+ val type: BookTypeEntity
)
\ No newline at end of file
diff --git a/app/src/main/java/com/mobyle/abbay/data/model/BookTypeEntity.kt b/app/src/main/java/com/mobyle/abbay/data/model/BookTypeEntity.kt
new file mode 100644
index 0000000..4690c8b
--- /dev/null
+++ b/app/src/main/java/com/mobyle/abbay/data/model/BookTypeEntity.kt
@@ -0,0 +1,6 @@
+package com.mobyle.abbay.data.model
+
+enum class BookTypeEntity {
+ FOLDER,
+ FILE
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mobyle/abbay/data/model/MultipleBooksEntity.kt b/app/src/main/java/com/mobyle/abbay/data/model/MultipleBooksEntity.kt
index c9f5f2d..05e6663 100644
--- a/app/src/main/java/com/mobyle/abbay/data/model/MultipleBooksEntity.kt
+++ b/app/src/main/java/com/mobyle/abbay/data/model/MultipleBooksEntity.kt
@@ -15,5 +15,6 @@ data class MultipleBooksEntity(
val duration: Long,
val currentBookPosition: Int = 0,
val speed: Float,
- val hasError: Boolean
+ val hasError: Boolean,
+ val type: BookTypeEntity
)
\ No newline at end of file
diff --git a/app/src/main/java/com/mobyle/abbay/data/repository/BooksRepositoryImpl.kt b/app/src/main/java/com/mobyle/abbay/data/repository/BooksRepositoryImpl.kt
index baccbbe..d14a98a 100644
--- a/app/src/main/java/com/mobyle/abbay/data/repository/BooksRepositoryImpl.kt
+++ b/app/src/main/java/com/mobyle/abbay/data/repository/BooksRepositoryImpl.kt
@@ -34,23 +34,17 @@ class BooksRepositoryImpl @Inject constructor(
}
override suspend fun getBookList(): List {
- val bookFilesList = localDataSource.getBookFilesList().map {
- it.toDomain()
- }
-
- val booksFolderList = localDataSource.getMultipleBooksList().map {
- it.toDomain()
- }
-
- return booksFolderList + bookFilesList
+ return localDataSource.getBooksList()
}
override suspend fun upsertBookList(booksList: List) {
val multipleBooksList = booksList.filterIsInstance().map { it.toEntity() }
val bookFilesList = booksList.filterIsInstance().map { it.toEntity() }
- localDataSource.addBookFileList(bookFilesList)
- localDataSource.addMultipleBooksList(multipleBooksList)
+ localDataSource.upsertBooksList(
+ singleFileBooksList = bookFilesList,
+ multipleBooksList = multipleBooksList
+ )
}
override suspend fun deleteBook(book: Book) {
diff --git a/app/src/main/java/com/mobyle/abbay/infra/navigation/AbbayNavHost.kt b/app/src/main/java/com/mobyle/abbay/infra/navigation/AbbayNavHost.kt
index 24bb2b8..be20791 100644
--- a/app/src/main/java/com/mobyle/abbay/infra/navigation/AbbayNavHost.kt
+++ b/app/src/main/java/com/mobyle/abbay/infra/navigation/AbbayNavHost.kt
@@ -7,16 +7,19 @@ import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.media3.session.MediaController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.mobyle.abbay.presentation.booklist.BooksListScreen
+import com.mobyle.abbay.presentation.booklist.BooksListViewModel
import com.mobyle.abbay.presentation.settings.SettingsScreen
@Composable
fun AbbayNavHost(
+ viewModel: BooksListViewModel = hiltViewModel(),
modifier: Modifier = Modifier,
navController: NavHostController,
player: MediaController,
@@ -33,6 +36,7 @@ fun AbbayNavHost(
route = NavigationItem.BookList.route,
) {
BooksListScreen(
+ viewModel = viewModel,
player = player,
openAppSettings = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
@@ -45,7 +49,9 @@ fun AbbayNavHost(
}
modal(NavigationItem.Settings.route) {
- SettingsScreen {
+ SettingsScreen(
+ booksViewModel = viewModel
+ ) {
navController.popBackStack()
}
}
diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt
index 97f28c4..98d9a2d 100644
--- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt
+++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt
@@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Intent
import android.media.MediaMetadataRetriever
import android.net.Uri
-import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -27,8 +26,6 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Error
-import androidx.compose.material.icons.filled.Pause
-import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
@@ -62,7 +59,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.getString
-import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.media3.common.PlaybackException
@@ -97,6 +93,7 @@ import com.mobyle.abbay.presentation.utils.prepareMultipleBooks
import com.mobyle.abbay.presentation.utils.toHHMMSS
import com.model.Book
import com.model.BookFile
+import com.model.BookType
import com.model.MultipleBooks
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -108,7 +105,7 @@ private const val AUTO_DENIAL_THRESHOLD = 300
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun BooksListScreen(
- viewModel: BooksListViewModel = hiltViewModel(),
+ viewModel: BooksListViewModel,
player: MediaController,
navigateToSettings: () -> Unit,
openAppSettings: () -> Unit,
@@ -128,9 +125,6 @@ fun BooksListScreen(
var componentHeight by remember { mutableStateOf(0.dp) }
val activity = LocalContext.current as Activity
val permissionRequestAttemptTime = remember { mutableLongStateOf(0) }
- val isGestureDisabled = remember {
- mutableStateOf(true)
- }
val permissionState = rememberMultiplePermissionsState(
permissions = viewModel.getPermissionsList(),
onPermissionsResult = { permissions ->
@@ -145,7 +139,7 @@ fun BooksListScreen(
val hasSelectedFolder by viewModel.hasSelectedFolder.collectAsState()
val isRefreshing by viewModel.isRefreshing.collectAsState()
val hasBookEnded by viewModel.showBookEndedDialog.collectAsState()
-
+ val isScreenLocked by viewModel.isScreenLocked.collectAsState()
LifecycleEventEffect(event = Lifecycle.Event.ON_CREATE) {
viewModel.shouldOpenPlayerInStartup()
}
@@ -206,10 +200,14 @@ fun BooksListScreen(
}
}
- metadataRetriever.toBook(context, id.orEmpty()).getThumb(context)
+ metadataRetriever.toBook(
+ context = context,
+ id = id.orEmpty(),
+ type = BookType.FILE
+ ).getThumb(context)
}
- viewModel.updateBookList(newBookList)
+ viewModel.updateBookList(newBookList + viewModel.booksList)
}
}
}
@@ -222,7 +220,10 @@ fun BooksListScreen(
viewModel.saveBookFolderPath(it.toString())
asyncScope.launch(Dispatchers.IO) {
delay(500)
- it.getBooks(context)?.let {
+ it.getBooks(
+ context = context,
+ type = BookType.FOLDER
+ )?.let {
val booksWithThumbnails = it.mapNotNull { book ->
book.getThumb(context)
}
@@ -235,13 +236,15 @@ fun BooksListScreen(
// SideEffects
BackHandler {
- asyncScope.launch {
- if (bottomSheetState.bottomSheetState.isCollapsed) {
- activity.finishAffinity()
- } else {
- bottomSheetState.bottomSheetState.collapse()
- }
+ if (!isScreenLocked) {
+ asyncScope.launch {
+ if (bottomSheetState.bottomSheetState.isCollapsed) {
+ activity.finishAffinity()
+ } else {
+ bottomSheetState.bottomSheetState.collapse()
+ }
+ }
}
}
@@ -265,6 +268,7 @@ fun BooksListScreen(
player.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
selectedBook?.let { book ->
+ player.stop()
viewModel.markBookAsError(book)
}
}
@@ -279,9 +283,8 @@ fun BooksListScreen(
if (selectedBook?.hasError == true) {
showErrorDialog.value = true
bottomSheetState.bottomSheetState.collapse()
+ viewModel.updateIsScreenLocked(false)
}
-
- isGestureDisabled.value = selectedBook?.hasError == false
}
BottomSheetScaffold(
@@ -296,6 +299,7 @@ fun BooksListScreen(
MiniPlayer(
player = player,
book = it,
+ isScreenLocked = isScreenLocked,
scaffoldState = bottomSheetState,
progress = it.progress,
onPlayingChange = { isPlaying ->
@@ -318,9 +322,7 @@ fun BooksListScreen(
)
}
},
- onDisableGesture = {
- isGestureDisabled.value = !it
- },
+ onLockScreen = viewModel::updateIsScreenLocked,
updateBookSpeed = {
selectedBook?.let { book ->
viewModel.updateBookSpeed(
@@ -340,7 +342,7 @@ fun BooksListScreen(
}
}
},
- sheetGesturesEnabled = isGestureDisabled.value,
+ sheetGesturesEnabled = !isScreenLocked,
sheetPeekHeight = if (hasBookSelected) 72.dp else 0.dp,
) {
Box(
@@ -383,7 +385,10 @@ fun BooksListScreen(
viewModel.setRefreshingLoading()
delay(500)
- uri.getBooks(context)?.let { books ->
+ uri.getBooks(
+ context = context,
+ type = BookType.FOLDER
+ )?.let { books ->
// Generate thumbnails for all books before checking for new ones
val booksWithThumbnails =
books.mapNotNull { book ->
@@ -513,7 +518,7 @@ fun BooksListScreen(
) {
Icon(
imageVector = Icons.Default.Delete,
- contentDescription = "Delete",
+ contentDescription = stringResource(R.string.delete_content_description_short),
tint = Color.White
)
}
@@ -683,6 +688,15 @@ fun BooksListScreen(
}
if (showErrorDialog.value) {
+ LaunchedEffect(Unit) {
+ player.stop()
+ selectedBook?.let {
+ if (selectedBook?.hasError == false) {
+ viewModel.markBookAsError(it)
+ }
+ }
+ }
+
AlertDialog(
onDismissRequest = {
showErrorDialog.value = false
@@ -735,7 +749,7 @@ fun BooksListScreen(
) {
Text(
stringResource(R.string.ok),
- color = MaterialTheme.colorScheme.primary
+ color = Color.White
)
}
}
diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListViewModel.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListViewModel.kt
index 18a6011..7495fdb 100644
--- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListViewModel.kt
+++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListViewModel.kt
@@ -7,6 +7,7 @@ import com.mobyle.abbay.infra.common.BaseViewModel
import com.mobyle.abbay.presentation.utils.permissions.CheckPermissionsProvider
import com.model.Book
import com.model.BookFile
+import com.model.BookType
import com.model.MultipleBooks
import com.usecase.DeleteBook
import com.usecase.GetBooksFolderPath
@@ -59,6 +60,8 @@ class BooksListViewModel @Inject constructor(
var shouldOpenPlayerInStartup = false
+ var isScreenLocked = MutableStateFlow(false)
+
private val _hasSelectedFolder = MutableStateFlow(getBooksFolderPath() != null)
val hasSelectedFolder: StateFlow get() = _hasSelectedFolder
@@ -79,19 +82,23 @@ class BooksListViewModel @Inject constructor(
observeBooksList(), hasPermissions
) { domainBookList, hasPermissions ->
val currentIds = booksList.map { it.id }.toSet()
- val newBooks = domainBookList.filter { book ->
- !currentIds.contains(book.id)
- }
+ val newBooks = domainBookList.map { book ->
+ if (currentIds.contains(book.id)) {
+ booksList.firstOrNull { it.id == book.id }?.let {
+ it
+ } ?: run {
+ book
+ }
+ } else {
+ book
+ }
+ }.distinctBy { it.id }
- if (domainBookList.isEmpty()) {
+ val state = if (hasPermissions) {
booksList.clear()
- }
+ booksList.addAll(newBooks.sortedBy { it.name })
- val state = if (hasPermissions) {
if (newBooks.isNotEmpty()) {
- booksList.addAll(newBooks)
- BooksListUiState.BookListSuccess(booksList.toList())
- } else if (booksList.isNotEmpty()) {
BooksListUiState.BookListSuccess(booksList.toList())
} else {
BooksListUiState.NoBookSelected
@@ -127,6 +134,15 @@ class BooksListViewModel @Inject constructor(
_hasSelectedFolder.value = true
}
+ fun addBooksFromNewFolder(filesList: List) {
+ val newBooks = filesList.distinctBy { it.id }
+ val individualBooks = booksList.filter { it.type == BookType.FILE }
+
+ updateBookList(newBooks + individualBooks)
+ _hasSelectedFolder.value = true
+ _isRefreshing.value = false
+ }
+
fun setCurrentProgress(
id: String,
progress: Long,
@@ -258,22 +274,29 @@ class BooksListViewModel @Inject constructor(
else -> book
}
booksList[index] = updatedBook
+
+ _uiState.tryEmit(BooksListUiState.BookListSuccess(booksList.toList()))
}
}
fun checkForNewBooks(newBooksList: List) {
val currentIds = booksList.map { it.id }.toSet()
- val newBooks = newBooksList.filter { book ->
- !currentIds.contains(book.id)
- }
+ val newBooks = newBooksList.map { book ->
+ if (currentIds.contains(book.id)) {
+ booksList.firstOrNull { it.id == book.id }?.let {
+ it
+ } ?: run {
+ book
+ }
+ } else {
+ book
+ }
+ }.distinctBy { it.id }
- if (newBooks.isNotEmpty()) {
- updateBookList(newBooks)
- } else {
- _uiState.tryEmit(BooksListUiState.BookListSuccess(booksList.toList()))
- _hasSelectedFolder.value = true
- }
+ val individualBooks = booksList.filter { it.type == BookType.FILE }
+ updateBookList(newBooks + individualBooks)
+ _hasSelectedFolder.value = true
_isRefreshing.value = false
}
@@ -296,6 +319,10 @@ class BooksListViewModel @Inject constructor(
_showBookEndedDialog.value = false
}
+ fun updateIsScreenLocked(isDisabled: Boolean) {
+ isScreenLocked.value = isDisabled
+ }
+
sealed class BooksListUiState {
data class BookListSuccess(val audiobookList: List) :
BooksListUiState()
diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/Utils.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/Utils.kt
index 400f03f..5136801 100644
--- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/Utils.kt
+++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/Utils.kt
@@ -15,6 +15,7 @@ import com.mobyle.abbay.presentation.utils.getFileName
import com.mobyle.abbay.presentation.utils.getId
import com.model.Book
import com.model.BookFile
+import com.model.BookType
import com.model.MultipleBooks
import java.io.File
@@ -48,7 +49,7 @@ fun Uri.resolveContentUri(context: Context): String? {
return File(base, split[1]).canonicalPath
}
-fun Uri.getBooks(context: Context): List? {
+fun Uri.getBooks(context: Context, type: BookType): List? {
return this.resolveContentUri(context)?.let { folderPath ->
val contentResolver: ContentResolver = context.contentResolver
val bookUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
@@ -78,7 +79,8 @@ fun Uri.getBooks(context: Context): List? {
thumbnail = thumbnail,
progress = progress,
duration = duration,
- speed = 1f
+ speed = 1f,
+ type = type
)
filesHashMap[fileFolderPath]?.let {
diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt
index 7764ef0..95e1d0c 100644
--- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt
+++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt
@@ -68,6 +68,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ExperimentalMotionApi
@@ -103,14 +104,14 @@ fun MiniPlayer(
book: Book,
onPlayingChange: (Boolean) -> Unit,
progress: Long,
+ isScreenLocked: Boolean,
scaffoldState: BottomSheetScaffoldState,
updateCurrentBookPosition: (Int) -> Unit,
updateProgress: (Long) -> Unit,
updateBookSpeed: (Float) -> Unit,
- onDisableGesture: (Boolean) -> Unit,
+ onLockScreen: (Boolean) -> Unit,
modifier: Modifier
) {
- val isScreenLocked = remember { mutableStateOf(false) }
val showUnlockDialog = remember { mutableStateOf(false) }
val playerIcon = remember {
@@ -146,13 +147,16 @@ fun MiniPlayer(
onPlayingChange(player.isPlaying)
}
+
Player.STATE_BUFFERING -> {
// Keep current icon during buffering
}
+
Player.STATE_ENDED -> {
playerIcon.value = Icons.Default.PlayArrow
onPlayingChange(false)
}
+
Player.STATE_IDLE -> {
playerIcon.value = Icons.Default.PlayArrow
onPlayingChange(false)
@@ -172,9 +176,9 @@ fun MiniPlayer(
}
}
}
-
+
player.addListener(listener)
-
+
// Clean up listener when the composable is disposed
// Note: In a real app, you might want to handle this differently
// depending on your lifecycle management
@@ -184,23 +188,19 @@ fun MiniPlayer(
SingleFilePlayer(
player = player,
book = book,
- isScreenLocked = isScreenLocked.value,
+ isScreenLocked = isScreenLocked,
onPlayingChange = onPlayingChange,
progress = progress,
scaffoldState = scaffoldState,
playerIcon = playerIcon,
onLockScreen = {
- if (it) {
- onDisableGesture(true)
- }
- isScreenLocked.value = it
+ onLockScreen(true)
},
showScreenLockedAlert = {
showUnlockDialog.value = true
},
unlockScreen = {
- onDisableGesture(false)
- isScreenLocked.value = false
+ onLockScreen(false)
},
updateProgress = updateProgress,
updateBookSpeed = updateBookSpeed,
@@ -210,27 +210,23 @@ fun MiniPlayer(
MultipleFilePlayer(
player = player,
book = book,
- isScreenLocked = isScreenLocked.value,
+ isScreenLocked = isScreenLocked,
onPlayingChange = onPlayingChange,
progress = progress,
scaffoldState = scaffoldState,
playerIcon = playerIcon,
updateProgress = updateProgress,
onLockScreen = {
- if (it) {
- onDisableGesture(true)
- }
- isScreenLocked.value = it
+ onLockScreen(true)
},
showScreenLockedAlert = {
showUnlockDialog.value = true
},
unlockScreen = {
- onDisableGesture(false)
- isScreenLocked.value = false
+ onLockScreen(false)
},
updateCurrentBookPosition = updateCurrentBookPosition,
- onDisableGesture = onDisableGesture,
+ onDisableGesture = onLockScreen,
updateBookSpeed = updateBookSpeed,
modifier = modifier
)
@@ -344,11 +340,14 @@ private fun SingleFilePlayer(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.4f))
- .combinedClickable(onClick = {
- showScreenLockedAlert()
- }, onLongClick = {
- unlockScreen()
- })
+ .combinedClickable(
+ onClick = {
+ showScreenLockedAlert()
+ },
+ onDoubleClick = {
+ unlockScreen()
+ }
+ )
)
}
}
@@ -462,12 +461,17 @@ private fun MultipleFilePlayer(
}
AnimatedVisibility(
- visible = swipeProgress == 1f, modifier = Modifier.padding(top = 8.dp)
+ visible = swipeProgress == 1f,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp)
) {
BookFileItem(
- file = files.getOrNull(currentIndex), onClick = {
+ file = files.getOrNull(currentIndex),
+ onClick = {
showChapters.value = !showChapters.value
- }, isExpanded = showChapters.value
+ },
+ isExpanded = showChapters.value
)
}
@@ -512,7 +516,8 @@ private fun MultipleFilePlayer(
.combinedClickable(
onClick = {
showScreenLockedAlert()
- }, onLongClick = {
+ },
+ onDoubleClick = {
unlockScreen()
}
)
@@ -533,12 +538,15 @@ private fun BookFileItem(
.fillMaxWidth()
.clickable { onClick() },
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
+ horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = file?.fileName ?: stringResource(R.string.unknown_file),
style = AbbayTextStyles.chapterTitle,
- maxLines = 1
+ textAlign = TextAlign.Center,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
+ modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
@@ -546,7 +554,8 @@ private fun BookFileItem(
contentDescription = if (isExpanded) stringResource(R.string.hide_chapters) else stringResource(
R.string.show_chapters
),
- tint = Color.White
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
)
}
}
@@ -602,7 +611,9 @@ private fun BookFilesList(
text = file.fileName,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.tertiary,
- maxLines = 1
+ textAlign = TextAlign.Start,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
)
Text(
text = file.duration.toHHMMSS(),
@@ -633,7 +644,7 @@ private fun PlayerController(
onPlayingChange(true)
player.play()
} else {
- if(!player.playWhenReady) {
+ if (!player.playWhenReady) {
player.prepareBook(id, position, MutableStateFlow(true))
}
player.addListener(object : Player.Listener {
@@ -693,7 +704,9 @@ private fun BooksTopBar(
text = currentSpeed.value.text, color = Color.White
)
Icon(
- Icons.Default.Speed, contentDescription = "Change speed", tint = Color.White
+ Icons.Default.Speed,
+ contentDescription = stringResource(R.string.change_speed_content_description),
+ tint = Color.White
)
}
}
@@ -825,7 +838,7 @@ private fun BookImage(
onPlayingChange(true)
player.play()
} else {
- if(!player.playWhenReady) {
+ if (!player.playWhenReady) {
player.prepareBook(book.id, progress, MutableStateFlow(true))
}
diff --git a/app/src/main/java/com/mobyle/abbay/presentation/common/mappers/ViewMappers.kt b/app/src/main/java/com/mobyle/abbay/presentation/common/mappers/ViewMappers.kt
index 18a6b68..d27cbcc 100644
--- a/app/src/main/java/com/mobyle/abbay/presentation/common/mappers/ViewMappers.kt
+++ b/app/src/main/java/com/mobyle/abbay/presentation/common/mappers/ViewMappers.kt
@@ -8,12 +8,17 @@ import android.net.Uri
import androidx.core.net.toUri
import com.mobyle.abbay.presentation.booklist.widgets.models.BookSpeed
import com.model.BookFile
+import com.model.BookType
import com.model.MultipleBooks
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
-fun MediaMetadataRetriever.toBook(context: Context, id: String): BookFile {
+fun MediaMetadataRetriever.toBook(
+ context: Context,
+ id: String,
+ type: BookType
+): BookFile {
val fileName = extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
val title = extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)
val duration = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) ?: "0L"
@@ -34,7 +39,8 @@ fun MediaMetadataRetriever.toBook(context: Context, id: String): BookFile {
},
progress = 0L,
duration = duration.toLong(),
- speed = 1f
+ speed = 1f,
+ type = type,
)
}
@@ -63,7 +69,8 @@ fun List.toMultipleBooks(): MultipleBooks? {
progress = 0L,
duration = this.sumOf { it.duration },
currentBookPosition = 0,
- speed = 1f
+ speed = 1f,
+ type = firstBook.type
)
}
}
@@ -91,7 +98,7 @@ fun getImageUriFromBitmap(context: Context, bitmap: Bitmap, name: String): Uri {
}
fun Float.toBookSpeed(): BookSpeed {
- return when(this) {
+ return when (this) {
0.5f -> BookSpeed.Half
1.25f -> BookSpeed.OnePointTwoFive
1.5f -> BookSpeed.OnePointFive
diff --git a/app/src/main/java/com/mobyle/abbay/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/mobyle/abbay/presentation/settings/SettingsScreen.kt
index 8c13ccb..03e8e9d 100644
--- a/app/src/main/java/com/mobyle/abbay/presentation/settings/SettingsScreen.kt
+++ b/app/src/main/java/com/mobyle/abbay/presentation/settings/SettingsScreen.kt
@@ -1,5 +1,7 @@
package com.mobyle.abbay.presentation.settings
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Switch
@@ -8,21 +10,55 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.mobyle.abbay.R
+import com.mobyle.abbay.presentation.booklist.BooksListViewModel
+import com.mobyle.abbay.presentation.booklist.getBooks
+import com.mobyle.abbay.presentation.booklist.getThumb
import com.mobyle.abbay.presentation.common.widgets.AbbayActionDialog
import com.mobyle.abbay.presentation.common.widgets.AbbayScreen
+import com.model.BookType
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
+ booksViewModel: BooksListViewModel,
close: () -> Unit
) {
val shouldPlayWhenAppIsClosed by viewModel.shouldPlayWhenAppIsClosed.collectAsState()
val shouldOpenPlayerInStartup by viewModel.shouldOpenPlayerInStartup.collectAsState()
val showShowDeleteConfirmation by viewModel.showShowDeleteConfirmation.collectAsState()
+ val asyncScope = rememberCoroutineScope()
+ val context = LocalContext.current
+
+ val openFolderSelector = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.OpenDocumentTree()
+ ) { uri ->
+ uri?.let {
+ booksViewModel.showLoading()
+ booksViewModel.saveBookFolderPath(it.toString())
+ asyncScope.launch(Dispatchers.IO) {
+ delay(500)
+ it.getBooks(
+ context = context,
+ type = BookType.FOLDER
+ )?.let {
+ val booksWithThumbnails = it.mapNotNull { book ->
+ book.getThumb(context)
+ }
+
+ booksViewModel.addBooksFromNewFolder(booksWithThumbnails)
+ }
+ }
+ }
+ }
AbbayScreen(
title = stringResource(R.string.settings),
@@ -60,6 +96,13 @@ fun SettingsScreen(
}
)
+ SettingItem(
+ text = stringResource(R.string.change_folder),
+ onClick = {
+ openFolderSelector.launch(null)
+ }
+ )
+
SettingItem(
text = stringResource(R.string.delete_all_books),
onClick = viewModel::showDeleteConfirmation
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000..4ef888a
--- /dev/null
+++ b/app/src/main/res/values-es/strings.xml
@@ -0,0 +1,73 @@
+
+
+ Abbay
+ Tus Audiolibros
+ Agregar carpeta
+ Agregar archivo
+ Agregar un Archivo
+ Agregar una Carpeta
+ o
+ Necesitamos permisos para acceder a tus libros
+ Conceder Permisos
+
+
+ Cancelar
+ OK
+ Eliminar
+ Descartar
+ Entendido
+ Libro Finalizado
+ El audiolibro ha terminado de reproducirse.
+
+
+ Configuración
+ Reproducir cuando la app esté cerrada
+ Mantener libro abierto al abrir la app
+ Change books folder
+ Eliminar todos los libros
+ Eliminar Todos los Libros
+ ¿Estás seguro de que quieres eliminar todos los libros? Esta acción no se puede deshacer.
+
+
+ Actualizar
+ Agregar
+ Agregar Carpeta
+ Agregar Archivo
+ Eliminar
+ Eliminar Libro
+ ¿Estás seguro de que quieres eliminar este libro?
+ Actualizar Libros
+ Esto actualizará tu lista de libros. Los libros agregados individualmente no se verán afectados.
+ Archivo No Encontrado
+ No se pudo encontrar el archivo de este libro.
+ El archivo puede haber sido movido o eliminado.
+
+
+ Archivo Desconocido
+ Mostrar Capítulos
+ Ocultar Capítulos
+ Cambiar Velocidad
+ Pantalla Bloqueada
+ Mantén presionado en cualquier lugar de la pantalla para desbloquear
+ Esto evita toques accidentales durante la narración
+
+
+ Error al Reproducir Archivo
+ Icono del Libro
+ Icono del Reloj
+
+
+ 0.5x
+ 1.0x
+ 1.25x
+ 1.5x
+ 2.0x
+
+
+
+
+
+ Eliminar
+ Cambiar velocidad
+
+
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
new file mode 100644
index 0000000..09fca7b
--- /dev/null
+++ b/app/src/main/res/values-pt/strings.xml
@@ -0,0 +1,73 @@
+
+
+ Abbay
+ Seus Audiobooks
+ Adicionar pasta
+ Adicionar arquivo
+ Adicionar um Arquivo
+ Adicionar uma Pasta
+ ou
+ Precisamos de permissões para acessar seus livros
+ Conceder Permissões
+
+
+ Cancelar
+ OK
+ Excluir
+ Descartar
+ Entendi
+ Livro Finalizado
+ O audiobook terminou de tocar.
+
+
+ Configurações
+ Tocar quando o app estiver fechado
+ Manter livro aberto ao abrir o app
+ Mudar pasta de livros
+ Excluir todos os livros
+ Excluir Todos os Livros
+ Tem certeza de que deseja excluir todos os livros? Esta ação não pode ser desfeita.
+
+
+ Atualizar
+ Adicionar
+ Adicionar Pasta
+ Adicionar Arquivo
+ Excluir
+ Excluir Livro
+ Tem certeza de que deseja excluir este livro?
+ Atualizar Livros
+ Isso atualizará a sua lista de livros. Livros adicionados individualmente não serão afetados.
+ Arquivo Não Encontrado
+ O arquivo deste livro não pôde ser encontrado.
+ O arquivo pode ter sido movido ou excluído.
+
+
+ Arquivo Desconhecido
+ Mostrar Capítulos
+ Ocultar Capítulos
+ Alterar Velocidade
+ Tela Bloqueada
+ Mantenha pressionado em qualquer lugar da tela para desbloquear
+ Isso evita toques acidentais durante a narração
+
+
+ Erro ao Reproduzir Arquivo
+ Ícone do Livro
+ Ícone do Relógio
+
+
+ 0.5x
+ 1.0x
+ 1.25x
+ 1.5x
+ 2.0x
+
+
+
+
+
+ Excluir
+ Alterar velocidade
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 630d2d4..4e3ca6c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -23,6 +23,7 @@
Play when app is closed
Open player on startup
Delete all books
+ Change books folder
Delete All Books
Are you sure you want to delete all books? This action cannot be undone.
@@ -63,5 +64,9 @@
+
+
+ Delete
+ Change speed
\ No newline at end of file
diff --git a/domain/src/main/java/com/model/Book.kt b/domain/src/main/java/com/model/Book.kt
index 3a675c9..19be2c1 100644
--- a/domain/src/main/java/com/model/Book.kt
+++ b/domain/src/main/java/com/model/Book.kt
@@ -8,5 +8,5 @@ interface Book {
val duration: Long
val speed: Float
val hasError: Boolean
+ val type: BookType
}
-
diff --git a/domain/src/main/java/com/model/BookFile.kt b/domain/src/main/java/com/model/BookFile.kt
index 87815f5..7322525 100644
--- a/domain/src/main/java/com/model/BookFile.kt
+++ b/domain/src/main/java/com/model/BookFile.kt
@@ -8,5 +8,6 @@ data class BookFile(
override val duration: Long,
override val speed: Float,
override val hasError: Boolean = false,
+ override val type: BookType,
val fileName: String
) : Book
\ No newline at end of file
diff --git a/domain/src/main/java/com/model/BookType.kt b/domain/src/main/java/com/model/BookType.kt
new file mode 100644
index 0000000..06c5c6f
--- /dev/null
+++ b/domain/src/main/java/com/model/BookType.kt
@@ -0,0 +1,6 @@
+package com.model
+
+enum class BookType {
+ FOLDER,
+ FILE
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/model/MultipleBooks.kt b/domain/src/main/java/com/model/MultipleBooks.kt
index b5fa4a7..4b575af 100644
--- a/domain/src/main/java/com/model/MultipleBooks.kt
+++ b/domain/src/main/java/com/model/MultipleBooks.kt
@@ -8,6 +8,7 @@ data class MultipleBooks(
override val duration: Long,
override val speed: Float,
override val hasError: Boolean = false,
+ override val type: BookType,
val bookFileList: List,
val currentBookPosition: Int = 0
) : Book
\ No newline at end of file