From 39ff7ef31036ff0eb5d72657dd6cf8400d6d3093 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sat, 16 Aug 2025 15:22:09 -0300 Subject: [PATCH 1/7] Add portuguese --- .idea/misc.xml | 6 + LOCALIZATION_README.md | 140 ++++++++++++++++++ .../presentation/booklist/BooksListScreen.kt | 4 +- .../booklist/widgets/MiniPlayer.kt | 2 +- app/src/main/res/values-pt/strings.xml | 72 +++++++++ app/src/main/res/values/strings.xml | 4 + 6 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 .idea/misc.xml create mode 100644 LOCALIZATION_README.md create mode 100644 app/src/main/res/values-pt/strings.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8f5c8ae --- /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..ba4ec39 --- /dev/null +++ b/LOCALIZATION_README.md @@ -0,0 +1,140 @@ +# Portuguese Localization Setup + +This project now supports Portuguese language localization alongside English. + +## 📁 File Structure + +``` +app/src/main/res/ +├── values/ +│ └── strings.xml # English strings (default) +└── values-pt/ + └── strings.xml # Portuguese 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 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. 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 | + +## 🚀 Adding More Languages + +To add another language (e.g., Spanish): + +1. Create `app/src/main/res/values-es/strings.xml` +2. Copy all strings from `values/strings.xml` +3. Translate each string to Spanish +4. The app will automatically support Spanish when the device language is set to Spanish + +## 📱 Testing Localization + +### Method 1: Device Settings +1. Go to Device Settings → System → Languages & input → Languages +2. Add Portuguese and move it to the top +3. Restart the app +4. All text should now appear in Portuguese + +### Method 2: Android Studio +1. Open Android Studio +2. Go to Run → Edit Configurations +3. In "Launch Options", set "Language" to "pt" +4. Run the app - it will launch in Portuguese + +### Method 3: ADB Command +```bash +adb shell setprop persist.sys.language pt +adb shell setprop persist.sys.country BR +``` + +## 🔍 Common Issues + +### Missing Translations +If a string is missing from the Portuguese 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 + +### RTL Support +Portuguese is 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 both 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 + +## 🛠️ Maintenance + +### Regular Tasks +- Review new strings added to English +- Translate new strings to Portuguese +- Test both 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 + +## 📖 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) 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..bb009ee 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 @@ -513,7 +513,7 @@ fun BooksListScreen( ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Delete", + contentDescription = stringResource(R.string.delete_content_description_short), tint = Color.White ) } @@ -735,7 +735,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/widgets/MiniPlayer.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt index 7764ef0..c93d1ea 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 @@ -693,7 +693,7 @@ 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 ) } } 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..2376897 --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,72 @@ + + + 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 + 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..944bc1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -63,5 +63,9 @@ + + + Delete + Change speed \ No newline at end of file From b6d9be15fea27d8e1ee5f05b2647ebe8eab2cd54 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sat, 16 Aug 2025 15:23:51 -0300 Subject: [PATCH 2/7] Add spanish --- LOCALIZATION_README.md | 71 ++++++++++++++++++------- app/src/main/res/values-es/strings.xml | 72 ++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 app/src/main/res/values-es/strings.xml diff --git a/LOCALIZATION_README.md b/LOCALIZATION_README.md index ba4ec39..f330f5f 100644 --- a/LOCALIZATION_README.md +++ b/LOCALIZATION_README.md @@ -1,6 +1,6 @@ -# Portuguese Localization Setup +# Multi-Language Localization Setup -This project now supports Portuguese language localization alongside English. +This project now supports multiple languages: English, Portuguese, and Spanish. ## 📁 File Structure @@ -8,8 +8,10 @@ This project now supports Portuguese language localization alongside English. app/src/main/res/ ├── values/ │ └── strings.xml # English strings (default) -└── values-pt/ - └── strings.xml # Portuguese strings +├── values-pt/ +│ └── strings.xml # Portuguese strings +└── values-es/ + └── strings.xml # Spanish strings ``` ## 🌐 How It Works @@ -17,6 +19,7 @@ app/src/main/res/ ### 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 @@ -53,7 +56,12 @@ android:label="@string/app_name" Nova Funcionalidade ``` -### 3. Use in Code +### 3. Add to Spanish (values-es/strings.xml) +```xml +Nueva Funcionalidad +``` + +### 4. Use in Code ```kotlin Text(stringResource(R.string.new_feature_title)) ``` @@ -64,40 +72,50 @@ Text(stringResource(R.string.new_feature_title)) |----------|------|------|---------| | 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., Spanish): +To add another language (e.g., French): -1. Create `app/src/main/res/values-es/strings.xml` +1. Create `app/src/main/res/values-fr/strings.xml` 2. Copy all strings from `values/strings.xml` -3. Translate each string to Spanish -4. The app will automatically support Spanish when the device language is set to Spanish +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 Portuguese and move it to the top +2. Add your desired language and move it to the top 3. Restart the app -4. All text should now appear in Portuguese +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 "pt" -4. Run the app - it will launch in Portuguese +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 the Portuguese file: +If a string is missing from a language file: - The app will fall back to English - Check the logcat for warnings about missing resources @@ -106,24 +124,26 @@ Some strings might need special handling for different languages: - Date formats - Number formats - Pluralization rules +- Gender-specific language variations ### RTL Support -Portuguese is left-to-right (LTR), so no special RTL handling is needed. +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 both files when adding new features +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 -- Test both languages regularly +- Translate new strings to Portuguese and Spanish +- Test all languages regularly - Update this documentation when adding new languages ### Quality Assurance @@ -131,6 +151,19 @@ Portuguese is left-to-right (LTR), so no special RTL handling is needed. - 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 @@ -138,3 +171,5 @@ Portuguese is left-to-right (LTR), so no special RTL handling is needed. - [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/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..244558b --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,72 @@ + + + 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 + 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 + + From 1aa25c4b2cb74b5ca0d042f7d7bcb2b3ee724cec Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sat, 16 Aug 2025 17:16:37 -0300 Subject: [PATCH 3/7] Update update books logic --- .../local/books/BooksLocalDataSource.kt | 14 +++--- .../local/books/BooksLocalDataSourceImpl.kt | 20 +++++---- .../data/datasource/local/daos/BooksDao.kt | 25 +++++++++++ .../data/repository/BooksRepositoryImpl.kt | 16 +++---- .../presentation/booklist/BooksListScreen.kt | 3 -- .../booklist/BooksListViewModel.kt | 45 +++++++++++-------- 6 files changed, 74 insertions(+), 49 deletions(-) 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..3887afa 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) 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..026ea31 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,26 @@ interface BooksDao { @Query("UPDATE MultipleBooksEntity SET progress = :progress, currentBookPosition = :currentPosition WHERE id = :id") suspend fun updateMultipleBookProgress(id: String, currentPosition: Int, progress: Long) + + /** + * Transaction: Delete all book files and insert new ones atomically + */ + @Transaction + suspend fun upsertBooksList( + multipleBooksList: List, + singleFileBooksList: List + ) { + deleteAllBookFiles() + 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/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/presentation/booklist/BooksListScreen.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt index bb009ee..895c6d8 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 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..7ff70d7 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 @@ -79,19 +79,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) - 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 @@ -263,17 +267,20 @@ class BooksListViewModel @Inject constructor( fun checkForNewBooks(newBooksList: List) { val currentIds = booksList.map { it.id }.toSet() - val newBooks = newBooksList.filter { book -> - !currentIds.contains(book.id) - } - - if (newBooks.isNotEmpty()) { - updateBookList(newBooks) - } else { - _uiState.tryEmit(BooksListUiState.BookListSuccess(booksList.toList())) - _hasSelectedFolder.value = true - } + 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 } + updateBookList(newBooks) + _hasSelectedFolder.value = true _isRefreshing.value = false } From 4d5c92a4060c751c7e962d18b8db3687baf2b10c Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sat, 16 Aug 2025 17:57:06 -0300 Subject: [PATCH 4/7] Keep individual books --- .idea/misc.xml | 2 +- .../data/mappers/DomainToEntityMappers.kt | 13 +++++++-- .../data/mappers/EntityToDomainMappers.kt | 13 +++++++-- .../mobyle/abbay/data/model/BookFileEntity.kt | 3 +- .../mobyle/abbay/data/model/BookTypeEntity.kt | 6 ++++ .../abbay/data/model/MultipleBooksEntity.kt | 3 +- .../presentation/booklist/BooksListScreen.kt | 29 ++++++++++++++++--- .../booklist/BooksListViewModel.kt | 9 ++++-- .../abbay/presentation/booklist/Utils.kt | 6 ++-- .../common/mappers/ViewMappers.kt | 15 +++++++--- domain/src/main/java/com/model/Book.kt | 2 +- domain/src/main/java/com/model/BookFile.kt | 1 + domain/src/main/java/com/model/BookType.kt | 6 ++++ .../src/main/java/com/model/MultipleBooks.kt | 1 + 14 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/mobyle/abbay/data/model/BookTypeEntity.kt create mode 100644 domain/src/main/java/com/model/BookType.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 8f5c8ae..cd7974c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file 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/presentation/booklist/BooksListScreen.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt index 895c6d8..2c90467 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 @@ -94,6 +94,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 @@ -203,10 +204,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) } } } @@ -219,7 +224,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) } @@ -262,6 +270,7 @@ fun BooksListScreen( player.addListener(object : Player.Listener { override fun onPlayerError(error: PlaybackException) { selectedBook?.let { book -> + player.stop() viewModel.markBookAsError(book) } } @@ -380,7 +389,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 -> @@ -680,6 +692,15 @@ fun BooksListScreen( } if (showErrorDialog.value) { + LaunchedEffect(Unit) { + player.stop() + selectedBook?.let { + if (selectedBook?.hasError == false) { + viewModel.markBookAsError(it) + } + } + } + AlertDialog( onDismissRequest = { showErrorDialog.value = false 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 7ff70d7..17520cf 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 @@ -93,7 +94,7 @@ class BooksListViewModel @Inject constructor( val state = if (hasPermissions) { booksList.clear() - booksList.addAll(newBooks) + booksList.addAll(newBooks.sortedBy { it.name }) if (newBooks.isNotEmpty()) { BooksListUiState.BookListSuccess(booksList.toList()) @@ -262,6 +263,8 @@ class BooksListViewModel @Inject constructor( else -> book } booksList[index] = updatedBook + + _uiState.tryEmit(BooksListUiState.BookListSuccess(booksList.toList())) } } @@ -279,7 +282,9 @@ class BooksListViewModel @Inject constructor( } }.distinctBy { it.id } - updateBookList(newBooks) + val individualBooks = booksList.filter { it.type == BookType.FILE } + + updateBookList(newBooks + individualBooks) _hasSelectedFolder.value = true _isRefreshing.value = false } 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/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/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 From 7177a13cea0c8e30a4c35cfe94461ca1f5a582a9 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sun, 17 Aug 2025 08:52:10 -0300 Subject: [PATCH 5/7] Fix remove all books --- .../local/books/BooksLocalDataSourceImpl.kt | 3 +- .../data/datasource/local/daos/BooksDao.kt | 12 ++++-- .../abbay/infra/navigation/AbbayNavHost.kt | 8 +++- .../presentation/booklist/BooksListScreen.kt | 2 +- .../booklist/BooksListViewModel.kt | 9 ++++ .../booklist/widgets/MiniPlayer.kt | 14 ++++-- .../presentation/settings/SettingsScreen.kt | 43 +++++++++++++++++++ app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 10 files changed, 82 insertions(+), 12 deletions(-) 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 3887afa..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 @@ -38,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 026ea31..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 @@ -49,15 +49,19 @@ interface BooksDao { @Query("UPDATE MultipleBooksEntity SET progress = :progress, currentBookPosition = :currentPosition WHERE id = :id") suspend fun updateMultipleBookProgress(id: String, currentPosition: Int, progress: Long) - /** - * Transaction: Delete all book files and insert new ones atomically - */ + @Transaction + suspend fun deleteAllBooks( + ) { + deleteAllBookFiles() + deleteAllMultipleBooks() + } + @Transaction suspend fun upsertBooksList( multipleBooksList: List, singleFileBooksList: List ) { - deleteAllBookFiles() + deleteAllBooks() insertMultipleBooksList(multipleBooksList) insertBookFilesList(singleFileBooksList) } 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 2c90467..89dbcb0 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 @@ -106,7 +106,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, 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 17520cf..1e34b37 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 @@ -132,6 +132,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, 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 c93d1ea..e641cee 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 @@ -462,12 +462,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 ) } @@ -538,7 +543,8 @@ private fun BookFileItem( Text( text = file?.fileName ?: stringResource(R.string.unknown_file), style = AbbayTextStyles.chapterTitle, - maxLines = 1 + textAlign = TextAlign.Center, + maxLines = 2 ) Spacer(modifier = Modifier.width(8.dp)) Icon( 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 index 244558b..4ef888a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -23,6 +23,7 @@ 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. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 2376897..09fca7b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -23,6 +23,7 @@ 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. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 944bc1c..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. From 9e7fc85c0691b0db047a4f0e851a1a7225d082a2 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sun, 17 Aug 2025 09:03:34 -0300 Subject: [PATCH 6/7] Fix book chapters UI --- .../presentation/booklist/widgets/MiniPlayer.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 e641cee..cff6a51 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 @@ -538,13 +539,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, textAlign = TextAlign.Center, - maxLines = 2 + overflow = TextOverflow.Ellipsis, + maxLines = 2, + modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(8.dp)) Icon( @@ -552,7 +555,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) ) } } @@ -608,7 +612,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(), From f6eda80b3ea4e7b22496e3d15fa896403ee54749 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sun, 17 Aug 2025 09:30:33 -0300 Subject: [PATCH 7/7] Update block screen behavior --- .../presentation/booklist/BooksListScreen.kt | 30 +++++----- .../booklist/BooksListViewModel.kt | 6 ++ .../booklist/widgets/MiniPlayer.kt | 57 ++++++++++--------- 3 files changed, 48 insertions(+), 45 deletions(-) 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 89dbcb0..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 @@ -59,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 @@ -126,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 -> @@ -143,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() } @@ -240,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() + } + } } } @@ -285,9 +283,8 @@ fun BooksListScreen( if (selectedBook?.hasError == true) { showErrorDialog.value = true bottomSheetState.bottomSheetState.collapse() + viewModel.updateIsScreenLocked(false) } - - isGestureDisabled.value = selectedBook?.hasError == false } BottomSheetScaffold( @@ -302,6 +299,7 @@ fun BooksListScreen( MiniPlayer( player = player, book = it, + isScreenLocked = isScreenLocked, scaffoldState = bottomSheetState, progress = it.progress, onPlayingChange = { isPlaying -> @@ -324,9 +322,7 @@ fun BooksListScreen( ) } }, - onDisableGesture = { - isGestureDisabled.value = !it - }, + onLockScreen = viewModel::updateIsScreenLocked, updateBookSpeed = { selectedBook?.let { book -> viewModel.updateBookSpeed( @@ -346,7 +342,7 @@ fun BooksListScreen( } } }, - sheetGesturesEnabled = isGestureDisabled.value, + sheetGesturesEnabled = !isScreenLocked, sheetPeekHeight = if (hasBookSelected) 72.dp else 0.dp, ) { Box( 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 1e34b37..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 @@ -60,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 @@ -317,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/widgets/MiniPlayer.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt index cff6a51..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 @@ -104,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 { @@ -147,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) @@ -173,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 @@ -185,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, @@ -211,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 ) @@ -345,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() + } + ) ) } } @@ -518,7 +516,8 @@ private fun MultipleFilePlayer( .combinedClickable( onClick = { showScreenLockedAlert() - }, onLongClick = { + }, + onDoubleClick = { unlockScreen() } ) @@ -645,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 { @@ -705,7 +704,9 @@ private fun BooksTopBar( text = currentSpeed.value.text, color = Color.White ) Icon( - Icons.Default.Speed, contentDescription = stringResource(R.string.change_speed_content_description), tint = Color.White + Icons.Default.Speed, + contentDescription = stringResource(R.string.change_speed_content_description), + tint = Color.White ) } } @@ -837,7 +838,7 @@ private fun BookImage( onPlayingChange(true) player.play() } else { - if(!player.playWhenReady) { + if (!player.playWhenReady) { player.prepareBook(book.id, progress, MutableStateFlow(true)) }