🚀 Browse Improvement, Room migration
Big BrowseScreen improvement: Filter, sort, list/grid, favorite directories, all files / directories. Room migration 5 → 6. Resolves: #14
This commit is contained in:
parent
07904d705c
commit
127d251ee6
56 changed files with 3408 additions and 542 deletions
|
|
@ -40,6 +40,7 @@ import ua.acclorite.book_story.presentation.screens.reader.ReaderScreenRoot
|
|||
import ua.acclorite.book_story.presentation.screens.settings.SettingsScreenRoot
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettingsRoot
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.BrowseSettingsRoot
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettingsRoot
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettingsRoot
|
||||
import ua.acclorite.book_story.presentation.screens.start.StartScreenRoot
|
||||
|
|
@ -180,6 +181,9 @@ class Activity : AppCompatActivity() {
|
|||
composable<Screen.Settings.ReaderSettings> {
|
||||
ReaderSettingsRoot()
|
||||
}
|
||||
composable<Screen.Settings.BrowseSettings> {
|
||||
BrowseSettingsRoot()
|
||||
}
|
||||
|
||||
// About screen
|
||||
composable<Screen.About> {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ object AppModule {
|
|||
.addMigrations(
|
||||
DatabaseHelper.MIGRATION_2_3, // creates LanguageHistoryEntity table(if does not exist)
|
||||
DatabaseHelper.MIGRATION_4_5, // creates ColorPresetEntity table(if does not exist)
|
||||
DatabaseHelper.MIGRATION_5_6, // creates FavoriteDirectoryEntity table(if does not exist)
|
||||
)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package ua.acclorite.book_story.data.local.dto
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class FavoriteDirectoryEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val path: String
|
||||
)
|
||||
|
|
@ -9,6 +9,7 @@ import androidx.room.Update
|
|||
import androidx.room.Upsert
|
||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
|
||||
import ua.acclorite.book_story.data.local.dto.FavoriteDirectoryEntity
|
||||
import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
||||
|
||||
@Dao
|
||||
|
|
@ -78,4 +79,14 @@ interface BookDao {
|
|||
|
||||
@Query("DELETE FROM colorpresetentity")
|
||||
suspend fun deleteColorPresets()
|
||||
|
||||
|
||||
@Upsert
|
||||
suspend fun insertFavoriteDirectory(favoriteDirectoryEntity: FavoriteDirectoryEntity)
|
||||
|
||||
@Query("SELECT EXISTS(SELECT 1 FROM favoritedirectoryentity WHERE path = :path LIMIT 1)")
|
||||
suspend fun favoriteDirectoryExits(path: String): Boolean
|
||||
|
||||
@Delete
|
||||
suspend fun deleteFavoriteDirectory(favoriteDirectoryEntity: FavoriteDirectoryEntity)
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import androidx.room.migration.Migration
|
|||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
|
||||
import ua.acclorite.book_story.data.local.dto.FavoriteDirectoryEntity
|
||||
import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
||||
|
||||
@Database(
|
||||
|
|
@ -17,13 +18,15 @@ import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
|||
BookEntity::class,
|
||||
HistoryEntity::class,
|
||||
ColorPresetEntity::class,
|
||||
FavoriteDirectoryEntity::class,
|
||||
],
|
||||
version = 5,
|
||||
version = 6,
|
||||
autoMigrations = [
|
||||
AutoMigration(1, 2),
|
||||
AutoMigration(2, 3),
|
||||
AutoMigration(3, 4, spec = DatabaseHelper.MIGRATION_3_4::class),
|
||||
AutoMigration(4, 5),
|
||||
AutoMigration(5, 6),
|
||||
],
|
||||
exportSchema = true
|
||||
)
|
||||
|
|
@ -68,4 +71,15 @@ object DatabaseHelper {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_5_6 = object : Migration(4, 5) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS `FavoriteDirectoryEntity` (" +
|
||||
"`path` TEXT NOT NULL, " +
|
||||
"PRIMARY KEY(`path`)" +
|
||||
")"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.local.data_store.DataStore
|
||||
import ua.acclorite.book_story.data.local.dto.FavoriteDirectoryEntity
|
||||
import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService
|
||||
import ua.acclorite.book_story.data.local.room.BookDao
|
||||
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
||||
|
|
@ -32,6 +33,7 @@ import ua.acclorite.book_story.domain.model.Book
|
|||
import ua.acclorite.book_story.domain.model.ColorPreset
|
||||
import ua.acclorite.book_story.domain.model.History
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
|
|
@ -464,19 +466,82 @@ class BookRepositoryImpl @Inject constructor(
|
|||
return result.await()
|
||||
}
|
||||
|
||||
override suspend fun getFilesFromDevice(query: String): Flow<Resource<List<File>>> {
|
||||
fun getAllFilesInDirectory(directory: File): List<File> {
|
||||
val filesList = mutableListOf<File>()
|
||||
override suspend fun getFilesFromDevice(query: String): List<SelectableFile> {
|
||||
val existingBooks = database
|
||||
.searchBooks("")
|
||||
.map { bookMapper.toBook(it) }
|
||||
val supportedExtensions = Constants.EXTENSIONS
|
||||
|
||||
val files = directory.listFiles()
|
||||
fun File.isValid(): Boolean {
|
||||
if (!exists()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isFileSupported = supportedExtensions.any { ext ->
|
||||
name.endsWith(
|
||||
ext,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
|
||||
if (!isFileSupported) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isFileNotAdded = existingBooks.all {
|
||||
it.filePath.lowercase().trim() != path.lowercase().trim()
|
||||
}
|
||||
|
||||
if (!isFileNotAdded) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isQuery = if (query.isEmpty()) true else name.trim().lowercase()
|
||||
.contains(query.trim().lowercase())
|
||||
|
||||
return isQuery
|
||||
}
|
||||
|
||||
suspend fun File.getAllFiles(): List<SelectableFile> {
|
||||
val filesList = mutableListOf<SelectableFile>()
|
||||
|
||||
val files = listFiles()
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (file.isFile) {
|
||||
filesList.add(file)
|
||||
if (!file.exists()) {
|
||||
continue
|
||||
}
|
||||
if (file.isDirectory) {
|
||||
val subDirectoryFiles = getAllFilesInDirectory(file)
|
||||
filesList.addAll(subDirectoryFiles)
|
||||
|
||||
when {
|
||||
file.isFile -> {
|
||||
if (file.isValid()) {
|
||||
filesList.add(
|
||||
SelectableFile(
|
||||
fileOrDirectory = file,
|
||||
parentDirectory = this,
|
||||
isDirectory = false,
|
||||
isFavorite = false,
|
||||
isSelected = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
file.isDirectory -> {
|
||||
val subDirectoryFiles = file.getAllFiles()
|
||||
if (subDirectoryFiles.isNotEmpty()) {
|
||||
filesList.add(
|
||||
SelectableFile(
|
||||
fileOrDirectory = file,
|
||||
parentDirectory = this,
|
||||
isDirectory = true,
|
||||
isFavorite = database.favoriteDirectoryExits(file.path),
|
||||
isSelected = false
|
||||
)
|
||||
)
|
||||
filesList.addAll(subDirectoryFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -484,77 +549,17 @@ class BookRepositoryImpl @Inject constructor(
|
|||
return filesList
|
||||
}
|
||||
|
||||
return flow {
|
||||
val existingBooks = database
|
||||
.searchBooks("")
|
||||
.map { bookMapper.toBook(it) }
|
||||
|
||||
val primaryDirectory = Environment.getExternalStorageDirectory()
|
||||
|
||||
if (
|
||||
!primaryDirectory.exists() ||
|
||||
(Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED &&
|
||||
Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED_READ_ONLY)
|
||||
) {
|
||||
emit(Resource.Success(null))
|
||||
return@flow
|
||||
}
|
||||
|
||||
val allFiles = getAllFilesInDirectory(primaryDirectory)
|
||||
|
||||
if (allFiles.isEmpty()) {
|
||||
emit(Resource.Success(null))
|
||||
return@flow
|
||||
}
|
||||
|
||||
val supportedExtensions = Constants.EXTENSIONS
|
||||
var filteredFiles = mutableListOf<File>()
|
||||
|
||||
allFiles.filter { file ->
|
||||
val isFileSupported = supportedExtensions.any { ext ->
|
||||
file.name.endsWith(
|
||||
ext,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
|
||||
if (!isFileSupported) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
val isFileNotAdded = existingBooks.all {
|
||||
it.filePath.substringAfterLast("/").lowercase() !=
|
||||
file.path.substringAfterLast("/").lowercase()
|
||||
}
|
||||
|
||||
if (!isFileNotAdded) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
val isQuery = if (query.isEmpty()) true else file.name.trim().lowercase()
|
||||
.contains(query.trim().lowercase())
|
||||
|
||||
if (!isQuery) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
true
|
||||
}.forEach {
|
||||
filteredFiles.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
filteredFiles = filteredFiles.sortedByDescending {
|
||||
it.lastModified()
|
||||
}.toMutableList()
|
||||
|
||||
emit(
|
||||
Resource.Success(
|
||||
data = filteredFiles
|
||||
)
|
||||
)
|
||||
val rootDirectory = Environment.getExternalStorageDirectory()
|
||||
if (
|
||||
!rootDirectory.exists() ||
|
||||
!rootDirectory.isDirectory ||
|
||||
(Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED &&
|
||||
Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED_READ_ONLY)
|
||||
) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
return rootDirectory.getAllFiles()
|
||||
}
|
||||
|
||||
override suspend fun getBooksFromFiles(files: List<File>): List<NullableBook> {
|
||||
|
|
@ -729,6 +734,19 @@ class BookRepositoryImpl @Inject constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateFavoriteDirectory(path: String) {
|
||||
if (database.favoriteDirectoryExits(path)) {
|
||||
database.deleteFavoriteDirectory(
|
||||
FavoriteDirectoryEntity(path)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
database.insertFavoriteDirectory(
|
||||
FavoriteDirectoryEntity(path)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package ua.acclorite.book_story.domain.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.util.Selected
|
||||
import java.io.File
|
||||
|
||||
@Immutable
|
||||
data class SelectableFile(
|
||||
val fileOrDirectory: File,
|
||||
val parentDirectory: File,
|
||||
val isDirectory: Boolean,
|
||||
val isFavorite: Boolean,
|
||||
val isSelected: Selected
|
||||
)
|
||||
|
|
@ -8,6 +8,7 @@ import ua.acclorite.book_story.domain.model.Book
|
|||
import ua.acclorite.book_story.domain.model.ColorPreset
|
||||
import ua.acclorite.book_story.domain.model.History
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
|
|
@ -71,7 +72,7 @@ interface BookRepository {
|
|||
|
||||
suspend fun getAllSettings(scope: CoroutineScope): MainState
|
||||
|
||||
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
|
||||
suspend fun getFilesFromDevice(query: String = ""): List<SelectableFile>
|
||||
|
||||
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>
|
||||
|
||||
|
|
@ -103,4 +104,7 @@ interface BookRepository {
|
|||
suspend fun reorderColorPresets(orderedColorPresets: List<ColorPreset>)
|
||||
|
||||
suspend fun deleteColorPreset(colorPreset: ColorPreset)
|
||||
|
||||
|
||||
suspend fun updateFavoriteDirectory(path: String)
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetBooksFromFiles @Inject constructor(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(files: List<File>): List<NullableBook> {
|
||||
return repository.getBooksFromFiles(files)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetFilesFromDevice @Inject constructor(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(query: String): Flow<Resource<List<File>>> {
|
||||
suspend fun execute(query: String): List<SelectableFile> {
|
||||
return repository.getFilesFromDevice(query)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class UpdateFavoriteDirectory @Inject constructor(private val repository: BookRepository) {
|
||||
suspend fun execute(path: String) {
|
||||
return repository.updateFavoriteDirectory(path)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ object Constants {
|
|||
const val MAIN_STATE = "main_state"
|
||||
|
||||
// Supported file extensions
|
||||
val EXTENSIONS = listOf(".txt", ".pdf", ".epub", ".fb2")
|
||||
val EXTENSIONS = listOf(".epub", ".pdf", ".fb2", ".txt")
|
||||
|
||||
// Supported languages
|
||||
val LANGUAGES = listOf(
|
||||
|
|
|
|||
|
|
@ -3,23 +3,36 @@ package ua.acclorite.book_story.domain.util
|
|||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
|
||||
object DataStoreConstants {
|
||||
// App settings
|
||||
// General settings
|
||||
val LANGUAGE = stringPreferencesKey("language")
|
||||
val THEME = stringPreferencesKey("theme")
|
||||
val DARK_THEME = stringPreferencesKey("dark_theme")
|
||||
val PURE_DARK = stringPreferencesKey("pure_dark")
|
||||
val THEME_CONTRAST = stringPreferencesKey("theme_contrast")
|
||||
val SHOW_START_SCREEN = booleanPreferencesKey("guide")
|
||||
val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates")
|
||||
|
||||
// Reader settings
|
||||
val DOUBLE_CLICK_TRANSLATION = booleanPreferencesKey("double_click_translation")
|
||||
val FAST_COLOR_PRESET_CHANGE = booleanPreferencesKey("fast_color_preset_change")
|
||||
val SIDE_PADDING = intPreferencesKey("side_padding")
|
||||
val FONT = stringPreferencesKey("font")
|
||||
val IS_ITALIC = booleanPreferencesKey("font_style")
|
||||
val FONT_SIZE = intPreferencesKey("font_size")
|
||||
val LINE_HEIGHT = intPreferencesKey("line_height")
|
||||
val PARAGRAPH_HEIGHT = intPreferencesKey("paragraph_height")
|
||||
val PARAGRAPH_INDENTATION = booleanPreferencesKey("paragraph_indentation")
|
||||
val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates")
|
||||
val SIDE_PADDING = intPreferencesKey("side_padding")
|
||||
val DOUBLE_CLICK_TRANSLATION = booleanPreferencesKey("double_click_translation")
|
||||
val FAST_COLOR_PRESET_CHANGE = booleanPreferencesKey("fast_color_preset_change")
|
||||
|
||||
// Browse settings
|
||||
val BROWSE_FILES_STRUCTURE = stringPreferencesKey("browse_files_structure")
|
||||
val BROWSE_LAYOUT = stringPreferencesKey("browse_layout")
|
||||
val BROWSE_AUTO_GRID_SIZE = booleanPreferencesKey("browse_auto_grid_size")
|
||||
val BROWSE_GRID_SIZE = intPreferencesKey("browse_grid_size")
|
||||
val BROWSE_PIN_FAVORITE_DIRECTORIES = booleanPreferencesKey("browse_pin_favorite_directories")
|
||||
val BROWSE_SORT_ORDER = stringPreferencesKey("browse_sort_order")
|
||||
val BROWSE_SORT_ORDER_DESCENDING = booleanPreferencesKey("browse_sort_order_descending")
|
||||
val BROWSE_INCLUDED_FILTER_ITEMS = stringSetPreferencesKey("browse_included_filter_items")
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package ua.acclorite.book_story.presentation.data
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class MainEvent {
|
||||
data class OnChangeLanguage(val lang: String) : MainEvent()
|
||||
data class OnChangeTheme(val theme: String) : MainEvent()
|
||||
|
|
@ -17,4 +20,12 @@ sealed class MainEvent {
|
|||
data class OnChangeSidePadding(val sidePadding: Int) : MainEvent()
|
||||
data class OnChangeDoubleClickTranslation(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeFastColorPresetChange(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeBrowseFilesStructure(val structure: String) : MainEvent()
|
||||
data class OnChangeBrowseLayout(val layout: String) : MainEvent()
|
||||
data class OnChangeBrowseAutoGridSize(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeBrowseGridSize(val size: Int) : MainEvent()
|
||||
data class OnChangeBrowsePinFavoriteDirectories(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeBrowseSortOrder(val order: String) : MainEvent()
|
||||
data class OnChangeBrowseSortOrderDescending(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeBrowseIncludedFilterItem(val item: String) : MainEvent()
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package ua.acclorite.book_story.presentation.data
|
||||
|
||||
import android.os.Build
|
||||
|
|
@ -7,6 +9,12 @@ import androidx.compose.runtime.Immutable
|
|||
import kotlinx.parcelize.Parcelize
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
import ua.acclorite.book_story.domain.util.DataStoreConstants
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseSortOrder
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseLayout
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseSortOrder
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toFilesStructure
|
||||
import ua.acclorite.book_story.presentation.ui.DarkTheme
|
||||
import ua.acclorite.book_story.presentation.ui.PureDark
|
||||
import ua.acclorite.book_story.presentation.ui.Theme
|
||||
|
|
@ -42,6 +50,14 @@ data class MainState(
|
|||
val sidePadding: Int? = null,
|
||||
val doubleClickTranslation: Boolean? = null,
|
||||
val fastColorPresetChange: Boolean? = null,
|
||||
val browseFilesStructure: BrowseFilesStructure? = null,
|
||||
val browseLayout: BrowseLayout? = null,
|
||||
val browseAutoGridSize: Boolean? = null,
|
||||
val browseGridSize: Int? = null,
|
||||
val browsePinFavoriteDirectories: Boolean? = null,
|
||||
val browseSortOrder: BrowseSortOrder? = null,
|
||||
val browseSortOrderDescending: Boolean? = null,
|
||||
val browseIncludedFilterItems: List<String>? = null,
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
/**
|
||||
|
|
@ -105,6 +121,37 @@ data class MainState(
|
|||
data[FAST_COLOR_PRESET_CHANGE.name] as? Boolean
|
||||
?: true
|
||||
|
||||
val browseFilesStructure: String = data[BROWSE_FILES_STRUCTURE.name] as? String
|
||||
?: BrowseFilesStructure.DIRECTORIES.name
|
||||
|
||||
val browseLayout: String = data[BROWSE_LAYOUT.name] as? String
|
||||
?: BrowseLayout.LIST.name
|
||||
|
||||
val browseAutoGridSize: Boolean =
|
||||
data[BROWSE_AUTO_GRID_SIZE.name] as? Boolean
|
||||
?: true
|
||||
|
||||
val browseGridSize: Int =
|
||||
data[BROWSE_GRID_SIZE.name] as? Int
|
||||
?: 0
|
||||
|
||||
val browsePinFavoriteDirectories: Boolean =
|
||||
data[BROWSE_PIN_FAVORITE_DIRECTORIES.name] as? Boolean
|
||||
?: true
|
||||
|
||||
val browseSortOrder: String =
|
||||
data[BROWSE_SORT_ORDER.name] as? String
|
||||
?: BrowseSortOrder.LAST_MODIFIED.name
|
||||
|
||||
val browseSortOrderDescending: Boolean =
|
||||
data[BROWSE_SORT_ORDER_DESCENDING.name] as? Boolean
|
||||
?: true
|
||||
|
||||
|
||||
val browseIncludedFilterItems =
|
||||
(data[BROWSE_INCLUDED_FILTER_ITEMS.name] as? Set<String>)?.toList()
|
||||
?: emptyList()
|
||||
|
||||
return MainState(
|
||||
language = language,
|
||||
theme = theme.toTheme(),
|
||||
|
|
@ -121,7 +168,15 @@ data class MainState(
|
|||
checkForUpdates = checkForUpdates,
|
||||
sidePadding = sidePadding,
|
||||
doubleClickTranslation = doubleClickTranslation,
|
||||
fastColorPresetChange = fastColorPresetChange
|
||||
fastColorPresetChange = fastColorPresetChange,
|
||||
browseFilesStructure = browseFilesStructure.toFilesStructure(),
|
||||
browseLayout = browseLayout.toBrowseLayout(),
|
||||
browseAutoGridSize = browseAutoGridSize,
|
||||
browseGridSize = browseGridSize,
|
||||
browsePinFavoriteDirectories = browsePinFavoriteDirectories,
|
||||
browseSortOrder = browseSortOrder.toBrowseSortOrder(),
|
||||
browseSortOrderDescending = browseSortOrderDescending,
|
||||
browseIncludedFilterItems = browseIncludedFilterItems
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ import ua.acclorite.book_story.domain.util.Constants
|
|||
import ua.acclorite.book_story.domain.util.DataStoreConstants
|
||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseLayout
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toBrowseSortOrder
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.toFilesStructure
|
||||
import ua.acclorite.book_story.presentation.ui.toDarkTheme
|
||||
import ua.acclorite.book_story.presentation.ui.toPureDark
|
||||
import ua.acclorite.book_story.presentation.ui.toTheme
|
||||
|
|
@ -228,6 +231,105 @@ class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeBrowseFilesStructure -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(DataStoreConstants.BROWSE_FILES_STRUCTURE, event.structure)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
browseFilesStructure = event.structure.toFilesStructure()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeBrowseLayout -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(DataStoreConstants.BROWSE_LAYOUT, event.layout)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
browseLayout = event.layout.toBrowseLayout()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeBrowseAutoGridSize -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(DataStoreConstants.BROWSE_AUTO_GRID_SIZE, event.bool)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
browseAutoGridSize = event.bool
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeBrowseGridSize -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(DataStoreConstants.BROWSE_GRID_SIZE, event.size)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
browseGridSize = event.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeBrowsePinFavoriteDirectories -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(
|
||||
DataStoreConstants.BROWSE_PIN_FAVORITE_DIRECTORIES,
|
||||
event.bool
|
||||
)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
browsePinFavoriteDirectories = event.bool
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeBrowseSortOrder -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(DataStoreConstants.BROWSE_SORT_ORDER, event.order)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
browseSortOrder = event.order.toBrowseSortOrder()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeBrowseSortOrderDescending -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(
|
||||
DataStoreConstants.BROWSE_SORT_ORDER_DESCENDING,
|
||||
event.bool
|
||||
)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
browseSortOrderDescending = event.bool
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeBrowseIncludedFilterItem -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val set = _state.value.browseIncludedFilterItems!!.toMutableSet()
|
||||
if (!set.add(event.item)) {
|
||||
set.remove(event.item)
|
||||
}
|
||||
|
||||
setDatastore.execute(DataStoreConstants.BROWSE_INCLUDED_FILTER_ITEMS, set)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
browseIncludedFilterItems = set.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,10 @@ sealed class Screen : Parcelable {
|
|||
@Parcelize
|
||||
@Keep
|
||||
data object ReaderSettings : Screen()
|
||||
|
||||
@Parcelize
|
||||
@Keep
|
||||
data object BrowseSettings : Screen()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
|
|
|||
|
|
@ -3,126 +3,125 @@ package ua.acclorite.book_story.presentation.screens.browse
|
|||
import android.Manifest
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.domain.util.OnNavigate
|
||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
||||
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
||||
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||
import ua.acclorite.book_story.presentation.components.CustomSearchTextField
|
||||
import ua.acclorite.book_story.presentation.components.MoreDropDown
|
||||
import ua.acclorite.book_story.presentation.components.customItems
|
||||
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
|
||||
import ua.acclorite.book_story.presentation.components.is_messages.IsError
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||
import ua.acclorite.book_story.presentation.data.Screen
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseFileItem
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseEmptyPlaceholder
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.BrowseStoragePermissionDialog
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.adding_dialog.BrowseAddingDialog
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.filter_bottom_sheet.BrowseFilterBottomSheet
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.layout.BrowseLayout
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.top_bar.BrowseTopBar
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure
|
||||
import ua.acclorite.book_story.presentation.ui.DefaultTransition
|
||||
import ua.acclorite.book_story.presentation.ui.Transitions
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun BrowseScreenRoot() {
|
||||
val navigator = LocalNavigator.current
|
||||
val viewModel: BrowseViewModel = hiltViewModel()
|
||||
val libraryViewModel: LibraryViewModel = hiltViewModel()
|
||||
val mainViewModel: MainViewModel = hiltViewModel()
|
||||
|
||||
val state = viewModel.state.collectAsState()
|
||||
val mainState = mainViewModel.state.collectAsState()
|
||||
|
||||
val permissionState = rememberPermissionState(
|
||||
permission = Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
val filteredFiles = remember(
|
||||
state.value.selectableFiles,
|
||||
state.value.selectedDirectory
|
||||
) {
|
||||
derivedStateOf {
|
||||
viewModel.filterList(mainState.value)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.onEvent(BrowseEvent.OnUpdateScrollOffset)
|
||||
viewModel.onEvent(
|
||||
BrowseEvent.OnPermissionCheck(permissionState)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
BrowseScreen(
|
||||
state = state,
|
||||
mainState = mainState,
|
||||
permissionState = permissionState,
|
||||
filteredFiles = filteredFiles.value,
|
||||
onNavigate = { navigator.it() },
|
||||
onEvent = viewModel::onEvent,
|
||||
onMainEvent = mainViewModel::onEvent,
|
||||
onLibraryEvent = libraryViewModel::onEvent
|
||||
)
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
viewModel.clearViewModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalPermissionsApi::class,
|
||||
ExperimentalMaterialApi::class
|
||||
)
|
||||
@Composable
|
||||
private fun BrowseScreen(
|
||||
state: State<BrowseState>,
|
||||
mainState: State<MainState>,
|
||||
permissionState: PermissionState,
|
||||
filteredFiles: List<SelectableFile>,
|
||||
onNavigate: OnNavigate,
|
||||
onEvent: (BrowseEvent) -> Unit,
|
||||
onMainEvent: (MainEvent) -> Unit,
|
||||
onLibraryEvent: (LibraryEvent) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val permissionState = rememberPermissionState(
|
||||
permission = Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = state.value.isRefreshing,
|
||||
onRefresh = {
|
||||
onEvent(BrowseEvent.OnRefreshList)
|
||||
}
|
||||
)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var showErrorMessage by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
onEvent(BrowseEvent.OnUpdateScrollOffset)
|
||||
onEvent(
|
||||
BrowseEvent.OnPermissionCheck(
|
||||
permissionState,
|
||||
hideErrorMessage = { showErrorMessage = false }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (state.value.requestPermissionDialog) {
|
||||
BrowseStoragePermissionDialog(onEvent, permissionState) {
|
||||
showErrorMessage = it
|
||||
}
|
||||
BrowseStoragePermissionDialog(onEvent, permissionState)
|
||||
}
|
||||
if (state.value.showAddingDialog) {
|
||||
BrowseAddingDialog(
|
||||
|
|
@ -131,6 +130,14 @@ private fun BrowseScreen(
|
|||
onLibraryEvent = onLibraryEvent
|
||||
)
|
||||
}
|
||||
if (state.value.showFilterBottomSheet) {
|
||||
BrowseFilterBottomSheet(
|
||||
state = state,
|
||||
mainState = mainState,
|
||||
onMainEvent = onMainEvent,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
|
|
@ -138,90 +145,11 @@ private fun BrowseScreen(
|
|||
.pullRefresh(refreshState),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
topBar = {
|
||||
AnimatedTopAppBar(
|
||||
scrollBehavior = null,
|
||||
isTopBarScrolled = state.value.hasSelectedItems || state.value.listState.canScrollBackward,
|
||||
|
||||
content1Visibility = !state.value.hasSelectedItems && !state.value.showSearch,
|
||||
content1NavigationIcon = {},
|
||||
content1Title = {
|
||||
Text(stringResource(id = R.string.browse_screen))
|
||||
},
|
||||
content1Actions = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Search,
|
||||
contentDescription = R.string.search_content_desc,
|
||||
disableOnClick = true
|
||||
) {
|
||||
onEvent(BrowseEvent.OnSearchShowHide)
|
||||
}
|
||||
MoreDropDown()
|
||||
},
|
||||
|
||||
content2Visibility = state.value.hasSelectedItems,
|
||||
content2NavigationIcon = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Clear,
|
||||
contentDescription = R.string.clear_selected_items_content_desc,
|
||||
disableOnClick = true
|
||||
) {
|
||||
onEvent(BrowseEvent.OnClearSelectedFiles)
|
||||
}
|
||||
},
|
||||
content2Title = {
|
||||
Text(
|
||||
stringResource(
|
||||
id = R.string.selected_items_count_query,
|
||||
state.value.selectedItemsCount.coerceAtLeast(1)
|
||||
),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
content2Actions = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Check,
|
||||
contentDescription = R.string.add_files_content_desc,
|
||||
disableOnClick = false,
|
||||
enabled = !state.value.showAddingDialog
|
||||
) {
|
||||
onEvent(BrowseEvent.OnAddingDialogRequest)
|
||||
}
|
||||
},
|
||||
|
||||
content3Visibility = state.value.showSearch && !state.value.hasSelectedItems,
|
||||
content3NavigationIcon = {
|
||||
CustomIconButton(
|
||||
icon = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = R.string.exit_search_content_desc,
|
||||
disableOnClick = true
|
||||
) {
|
||||
onEvent(BrowseEvent.OnSearchShowHide)
|
||||
}
|
||||
},
|
||||
content3Title = {
|
||||
CustomSearchTextField(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.onGloballyPositioned {
|
||||
onEvent(BrowseEvent.OnRequestFocus(focusRequester))
|
||||
},
|
||||
query = state.value.searchQuery,
|
||||
onQueryChange = {
|
||||
onEvent(BrowseEvent.OnSearchQueryChange(it))
|
||||
},
|
||||
onSearch = {
|
||||
onEvent(BrowseEvent.OnSearch)
|
||||
},
|
||||
placeholder = stringResource(
|
||||
id = R.string.search_query,
|
||||
stringResource(id = R.string.files)
|
||||
)
|
||||
)
|
||||
},
|
||||
content3Actions = {
|
||||
MoreDropDown()
|
||||
}
|
||||
BrowseTopBar(
|
||||
state = state,
|
||||
mainState = mainState,
|
||||
filteredFiles = filteredFiles,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
|
|
@ -231,86 +159,81 @@ private fun BrowseScreen(
|
|||
.padding(top = padding.calculateTopPadding())
|
||||
) {
|
||||
DefaultTransition(visible = !state.value.isLoading) {
|
||||
LazyColumn(
|
||||
state = state.value.listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
customItems(
|
||||
state.value.selectableFiles,
|
||||
key = { it.first.path }
|
||||
) { selectableFile ->
|
||||
BrowseFileItem(
|
||||
file = selectableFile,
|
||||
modifier = Modifier.animateItem(),
|
||||
hasSelectedFiles = state.value.selectableFiles.any { it.second },
|
||||
onLongClick = {
|
||||
BrowseLayout(
|
||||
state = state,
|
||||
mainState = mainState,
|
||||
filteredFiles = filteredFiles,
|
||||
onLongItemClick = { selectableFile ->
|
||||
when (selectableFile.isDirectory) {
|
||||
false -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(
|
||||
R.string.file_path_query,
|
||||
selectableFile.first.path
|
||||
selectableFile.fileOrDirectory.path
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
},
|
||||
onClick = {
|
||||
onEvent(BrowseEvent.OnSelectFile(selectableFile))
|
||||
}
|
||||
|
||||
true -> {
|
||||
onEvent(
|
||||
BrowseEvent.OnSelectFile(
|
||||
includedFileFormats = mainState.value.browseIncludedFilterItems!!,
|
||||
file = selectableFile
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFavoriteItemClick = {
|
||||
onEvent(
|
||||
BrowseEvent.OnUpdateFavoriteDirectory(
|
||||
it.fileOrDirectory.path
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onItemClick = { selectableFile ->
|
||||
when (selectableFile.isDirectory) {
|
||||
false -> {
|
||||
onEvent(
|
||||
BrowseEvent.OnSelectFile(
|
||||
includedFileFormats = mainState.value.browseIncludedFilterItems!!,
|
||||
file = selectableFile
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomAnimatedVisibility(
|
||||
visible = showErrorMessage,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
enter = Transitions.DefaultTransitionIn,
|
||||
exit = fadeOut(tween(0))
|
||||
) {
|
||||
IsError(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
errorMessage = stringResource(id = R.string.error_permission),
|
||||
icon = painterResource(id = R.drawable.error),
|
||||
actionTitle = stringResource(id = R.string.grant_permission)
|
||||
) {
|
||||
onEvent(
|
||||
BrowseEvent.OnPermissionCheck(
|
||||
permissionState,
|
||||
hideErrorMessage = { showErrorMessage = false }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CustomAnimatedVisibility(
|
||||
visible = !state.value.isLoading && state.value.selectableFiles.isEmpty()
|
||||
&& !showErrorMessage && !state.value.requestPermissionDialog
|
||||
&& !state.value.isRefreshing,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
enter = Transitions.DefaultTransitionIn,
|
||||
exit = fadeOut(tween(0))
|
||||
) {
|
||||
IsEmpty(
|
||||
message = stringResource(id = R.string.browse_empty),
|
||||
icon = painterResource(id = R.drawable.empty_browse),
|
||||
actionTitle = stringResource(id = R.string.get_help),
|
||||
action = {
|
||||
onNavigate {
|
||||
navigate(Screen.Help(false))
|
||||
true -> {
|
||||
if (!state.value.hasSelectedItems) {
|
||||
onEvent(
|
||||
BrowseEvent.OnChangeDirectory(
|
||||
selectableFile.fileOrDirectory,
|
||||
savePreviousDirectory = true
|
||||
)
|
||||
)
|
||||
} else {
|
||||
onEvent(
|
||||
BrowseEvent.OnSelectFile(
|
||||
includedFileFormats = mainState.value.browseIncludedFilterItems!!,
|
||||
file = selectableFile
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
BrowseEmptyPlaceholder(
|
||||
state = state,
|
||||
isFilesEmpty = filteredFiles.isEmpty(),
|
||||
storagePermissionState = permissionState,
|
||||
onNavigate = onNavigate,
|
||||
onEvent = onEvent
|
||||
)
|
||||
|
||||
PullRefreshIndicator(
|
||||
state.value.isRefreshing,
|
||||
refreshState,
|
||||
|
|
@ -332,17 +255,18 @@ private fun BrowseScreen(
|
|||
return@BackHandler
|
||||
}
|
||||
|
||||
if (
|
||||
state.value.inNestedDirectory
|
||||
&& mainState.value.browseFilesStructure != BrowseFilesStructure.ALL_FILES
|
||||
) {
|
||||
onEvent(
|
||||
BrowseEvent.OnGoBackDirectory
|
||||
)
|
||||
return@BackHandler
|
||||
}
|
||||
|
||||
onNavigate {
|
||||
navigate(Screen.Library)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.util.OnNavigate
|
||||
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
||||
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
|
||||
import ua.acclorite.book_story.presentation.components.is_messages.IsError
|
||||
import ua.acclorite.book_story.presentation.data.Screen
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
|
||||
import ua.acclorite.book_story.presentation.ui.Transitions
|
||||
|
||||
/**
|
||||
* Browse Empty Placeholder.
|
||||
* Shows error or empty message.
|
||||
*
|
||||
* @param state [BrowseState].
|
||||
* @param isFilesEmpty Whether the list is empty.
|
||||
* @param storagePermissionState Storage [PermissionState].
|
||||
* @param onNavigate Navigator callback.
|
||||
* @param onEvent [BrowseEvent] callback.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun BoxScope.BrowseEmptyPlaceholder(
|
||||
state: State<BrowseState>,
|
||||
isFilesEmpty: Boolean,
|
||||
storagePermissionState: PermissionState,
|
||||
onNavigate: OnNavigate,
|
||||
onEvent: (BrowseEvent) -> Unit
|
||||
) {
|
||||
CustomAnimatedVisibility(
|
||||
visible = state.value.isError,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
enter = Transitions.DefaultTransitionIn,
|
||||
exit = Transitions.NoExitAnimation
|
||||
) {
|
||||
IsError(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
errorMessage = stringResource(id = R.string.error_permission),
|
||||
icon = painterResource(id = R.drawable.error),
|
||||
actionTitle = stringResource(id = R.string.grant_permission)
|
||||
) {
|
||||
onEvent(
|
||||
BrowseEvent.OnPermissionCheck(storagePermissionState)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CustomAnimatedVisibility(
|
||||
visible = !state.value.isLoading
|
||||
&& isFilesEmpty
|
||||
&& !state.value.isError
|
||||
&& !state.value.requestPermissionDialog
|
||||
&& !state.value.isRefreshing,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
enter = Transitions.DefaultTransitionIn,
|
||||
exit = Transitions.NoExitAnimation
|
||||
) {
|
||||
IsEmpty(
|
||||
message = stringResource(id = R.string.browse_empty),
|
||||
icon = painterResource(id = R.drawable.empty_browse),
|
||||
actionTitle = stringResource(id = R.string.get_help),
|
||||
action = {
|
||||
onNavigate {
|
||||
navigate(Screen.Help(false))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.util.Selected
|
||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||
import ua.acclorite.book_story.presentation.ui.DefaultTransition
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Browse list element item. Can be selected.
|
||||
*
|
||||
* @param file [File].
|
||||
* @param hasSelectedFiles Whether parent list has selected items.
|
||||
* @param modifier Modifier.
|
||||
* @param onClick OnClick callback.
|
||||
* @param onLongClick OnLongClick callback.
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BrowseFileItem(
|
||||
file: Pair<File, Selected>,
|
||||
hasSelectedFiles: Boolean,
|
||||
modifier: Modifier,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) {
|
||||
val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) }
|
||||
val lastModified = rememberSaveable { pattern.format(Date(file.first.lastModified())) }
|
||||
val sizeBytes = rememberSaveable { file.first.length() }
|
||||
|
||||
val fileSizeKB = rememberSaveable { if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0 }
|
||||
val fileSizeMB = rememberSaveable { if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0 }
|
||||
val fileSize = rememberSaveable {
|
||||
if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB)
|
||||
else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB)
|
||||
else "0 KB"
|
||||
}
|
||||
|
||||
val outlineColor = if (file.second) MaterialTheme.colorScheme.outline
|
||||
else MaterialTheme.colorScheme.outlineVariant
|
||||
val backgroundColor = if (file.second) MaterialTheme.colorScheme.secondaryContainer
|
||||
else Color.Transparent
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 3.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(backgroundColor, RoundedCornerShape(10.dp))
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
},
|
||||
onClick = {
|
||||
onClick()
|
||||
}
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 7.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(0.88f)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
1.dp,
|
||||
outlineColor,
|
||||
RoundedCornerShape(6.dp)
|
||||
)
|
||||
.padding(14.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||
contentDescription = stringResource(id = R.string.file_icon_content_desc),
|
||||
modifier = Modifier
|
||||
.size(22.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
file.first.name,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
lineHeight = 18.sp,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
"$fileSize, $lastModified",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(0.12f)
|
||||
.padding(end = 6.dp),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
DefaultTransition(
|
||||
visible = hasSelectedFiles
|
||||
) {
|
||||
CustomCheckbox(
|
||||
selected = file.second,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
size = 18.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,13 +14,15 @@ import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
|
|||
|
||||
/**
|
||||
* Storage permission dialog.
|
||||
*
|
||||
* @param onEvent [BrowseEvent] callback.
|
||||
* @param permissionState Storage [PermissionState].
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun BrowseStoragePermissionDialog(
|
||||
onEvent: (BrowseEvent) -> Unit,
|
||||
permissionState: PermissionState,
|
||||
showErrorMessage: (Boolean) -> Unit
|
||||
permissionState: PermissionState
|
||||
) {
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
|
||||
|
|
@ -32,8 +34,7 @@ fun BrowseStoragePermissionDialog(
|
|||
onDismiss = {
|
||||
onEvent(
|
||||
BrowseEvent.OnStoragePermissionDismiss(
|
||||
permissionState,
|
||||
showErrorMessage = { showErrorMessage(true) }
|
||||
permissionState = permissionState
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
@ -43,9 +44,8 @@ fun BrowseStoragePermissionDialog(
|
|||
onAction = {
|
||||
onEvent(
|
||||
BrowseEvent.OnStoragePermissionRequest(
|
||||
activity,
|
||||
storagePermissionState = permissionState,
|
||||
hideErrorMessage = { showErrorMessage(false) }
|
||||
activity = activity,
|
||||
storagePermissionState = permissionState
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.filter_bottom_sheet
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.presentation.components.CustomBottomSheet
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseFilterSubcategory
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseGeneralSubcategory
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseSortSubcategory
|
||||
|
||||
/**
|
||||
* Browse Filter Bottom Sheet.
|
||||
* Lets user sort, filter and change appearance of the Browse.
|
||||
*
|
||||
* @param state [BrowseState].
|
||||
* @param mainState [MainState].
|
||||
* @param onMainEvent [MainEvent] callback.
|
||||
* @param onEvent [BrowseEvent] callback.
|
||||
*/
|
||||
@Composable
|
||||
fun BrowseFilterBottomSheet(
|
||||
state: State<BrowseState>,
|
||||
mainState: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit,
|
||||
onEvent: (BrowseEvent) -> Unit
|
||||
) {
|
||||
val pagerState = rememberPagerState(state.value.currentPage) { 3 }
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
onEvent(
|
||||
BrowseEvent.OnScrollToFilterPage(
|
||||
page = pagerState.currentPage,
|
||||
pagerState = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
CustomBottomSheet(
|
||||
hasFixedHeight = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.5f),
|
||||
dragHandle = {},
|
||||
onDismissRequest = {
|
||||
onEvent(BrowseEvent.OnShowHideFilterBottomSheet)
|
||||
}
|
||||
) {
|
||||
BrowseFilterBottomSheetTabRow(
|
||||
onEvent = onEvent,
|
||||
pagerState = pagerState
|
||||
)
|
||||
|
||||
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page ->
|
||||
when (page) {
|
||||
0 -> {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
BrowseGeneralSubcategory(
|
||||
state = mainState,
|
||||
onMainEvent = onMainEvent,
|
||||
showTitle = false,
|
||||
topPadding = 16.dp,
|
||||
bottomPadding = 8.dp + it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
1 -> {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
BrowseFilterSubcategory(
|
||||
state = mainState,
|
||||
onMainEvent = onMainEvent,
|
||||
showTitle = false,
|
||||
topPadding = 16.dp,
|
||||
bottomPadding = 8.dp + it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
2 -> {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
BrowseSortSubcategory(
|
||||
state = mainState,
|
||||
onMainEvent = onMainEvent,
|
||||
showTitle = false,
|
||||
topPadding = 16.dp,
|
||||
bottomPadding = 8.dp + it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.filter_bottom_sheet
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
|
||||
|
||||
/**
|
||||
* Browse Filter Bottom Sheet Tab Row.
|
||||
* Lets user switch tabs in bottom sheet.
|
||||
*
|
||||
* @param onEvent [BrowseEvent] callback.
|
||||
* @param pagerState [PagerState].
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BrowseFilterBottomSheetTabRow(
|
||||
onEvent: (BrowseEvent) -> Unit,
|
||||
pagerState: PagerState
|
||||
) {
|
||||
val tabItems = listOf(
|
||||
stringResource(id = R.string.general_tab),
|
||||
stringResource(id = R.string.filter_tab),
|
||||
stringResource(id = R.string.sort_tab)
|
||||
)
|
||||
|
||||
PrimaryTabRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
containerColor = Color.Transparent
|
||||
) {
|
||||
tabItems.forEachIndexed { index, tabItem ->
|
||||
Tab(
|
||||
modifier = Modifier,
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = {
|
||||
onEvent(BrowseEvent.OnScrollToFilterPage(index, pagerState))
|
||||
},
|
||||
selectedContentColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
text = {
|
||||
Text(
|
||||
tabItem,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.layout.grid.BrowseGridItem
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.layout.list.BrowseListItem
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Browse list element item. Can be selected.
|
||||
*
|
||||
* @param file [File].
|
||||
* @param hasSelectedFiles Whether parent list has selected items.
|
||||
* @param modifier Modifier.
|
||||
* @param onClick OnClick callback.
|
||||
* @param onLongClick OnLongClick callback.
|
||||
*/
|
||||
@Composable
|
||||
fun BrowseItem(
|
||||
layout: BrowseLayout,
|
||||
file: SelectableFile,
|
||||
hasSelectedFiles: Boolean,
|
||||
modifier: Modifier,
|
||||
onClick: () -> Unit,
|
||||
onFavoriteClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) {
|
||||
when (layout) {
|
||||
BrowseLayout.LIST -> {
|
||||
BrowseListItem(
|
||||
modifier = modifier,
|
||||
file = file,
|
||||
hasSelectedFiles = hasSelectedFiles,
|
||||
onClick = onClick,
|
||||
onFavoriteClick = onFavoriteClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
}
|
||||
|
||||
BrowseLayout.GRID -> {
|
||||
BrowseGridItem(
|
||||
modifier = modifier,
|
||||
file = file,
|
||||
hasSelectedFiles = hasSelectedFiles,
|
||||
onClick = onClick,
|
||||
onFavoriteClick = onFavoriteClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.layout.grid.BrowseGridLayout
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.layout.list.BrowseListLayout
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout
|
||||
|
||||
/**
|
||||
* Browse Layout.
|
||||
* Shows Grid or List.
|
||||
*
|
||||
* @param state [BrowseState].
|
||||
* @param mainState [MainState].
|
||||
* @param filteredFiles Filtered items.
|
||||
* @param onLongItemClick OnLongItemClick callback.
|
||||
* @param onFavoriteItemClick OnFavoriteItemClick callback.
|
||||
* @param onItemClick OnItemClick callback.
|
||||
*/
|
||||
@Composable
|
||||
fun BrowseLayout(
|
||||
state: State<BrowseState>,
|
||||
mainState: State<MainState>,
|
||||
filteredFiles: List<SelectableFile>,
|
||||
onLongItemClick: (SelectableFile) -> Unit,
|
||||
onFavoriteItemClick: (SelectableFile) -> Unit,
|
||||
onItemClick: (SelectableFile) -> Unit
|
||||
) {
|
||||
when (mainState.value.browseLayout!!) {
|
||||
BrowseLayout.LIST -> {
|
||||
BrowseListLayout(
|
||||
state = state,
|
||||
filteredFiles = filteredFiles,
|
||||
onLongItemClick = onLongItemClick,
|
||||
onFavoriteItemClick = onFavoriteItemClick,
|
||||
onItemClick = onItemClick
|
||||
)
|
||||
}
|
||||
|
||||
BrowseLayout.GRID -> {
|
||||
BrowseGridLayout(
|
||||
state = state,
|
||||
gridSize = mainState.value.browseGridSize!!,
|
||||
autoGridSize = mainState.value.browseAutoGridSize!!,
|
||||
filteredFiles = filteredFiles,
|
||||
onLongItemClick = onLongItemClick,
|
||||
onFavoriteItemClick = onFavoriteItemClick,
|
||||
onItemClick = onItemClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout.grid
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||
import ua.acclorite.book_story.presentation.ui.DefaultTransition
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun BrowseGridDirectoryItem(
|
||||
file: SelectableFile,
|
||||
hasSelectedFiles: Boolean,
|
||||
onFavoriteClick: () -> Unit
|
||||
) {
|
||||
val lastModified = rememberSaveable {
|
||||
SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
||||
.format(Date(file.fileOrDirectory.lastModified()))
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
1.dp,
|
||||
if (file.isSelected) MaterialTheme.colorScheme.outline
|
||||
else MaterialTheme.colorScheme.outlineVariant,
|
||||
RoundedCornerShape(10.dp)
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Folder,
|
||||
contentDescription = stringResource(id = R.string.directory_icon_content_desc),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.3f)
|
||||
.aspectRatio(1f),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
DefaultTransition(
|
||||
visible = hasSelectedFiles,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
CustomCheckbox(
|
||||
selected = file.isSelected,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
size = 18.dp
|
||||
)
|
||||
}
|
||||
DefaultTransition(
|
||||
visible = !hasSelectedFiles,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (file.isFavorite) Icons.Default.Favorite
|
||||
else Icons.Default.FavoriteBorder,
|
||||
contentDescription = stringResource(
|
||||
id = R.string.favorite_directory_content_desc
|
||||
),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable(
|
||||
enabled = !hasSelectedFiles,
|
||||
interactionSource = null,
|
||||
indication = null
|
||||
) {
|
||||
onFavoriteClick()
|
||||
},
|
||||
tint = if (file.isFavorite) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
file.fileOrDirectory.name,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
lineHeight = 18.sp,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
lastModified,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout.grid
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||
import ua.acclorite.book_story.presentation.ui.DefaultTransition
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun BrowseGridFileItem(file: SelectableFile, hasSelectedFiles: Boolean) {
|
||||
val lastModified = rememberSaveable {
|
||||
SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
||||
.format(Date(file.fileOrDirectory.lastModified()))
|
||||
}
|
||||
|
||||
val sizeBytes = rememberSaveable { file.fileOrDirectory.length() }
|
||||
val fileSizeKB = rememberSaveable {
|
||||
if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0
|
||||
}
|
||||
val fileSizeMB = rememberSaveable {
|
||||
if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0
|
||||
}
|
||||
val fileSize = rememberSaveable {
|
||||
if (fileSizeMB >= 1.0) "%.0f MB".format(fileSizeMB)
|
||||
else if (fileSizeMB > 0.0) "%.0f KB".format(fileSizeKB)
|
||||
else "0 KB"
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
1.dp,
|
||||
if (file.isSelected) MaterialTheme.colorScheme.outline
|
||||
else MaterialTheme.colorScheme.outlineVariant,
|
||||
RoundedCornerShape(10.dp)
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||
contentDescription = stringResource(id = R.string.file_icon_content_desc),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.3f)
|
||||
.aspectRatio(1f),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
DefaultTransition(
|
||||
visible = hasSelectedFiles,
|
||||
modifier = Modifier.align(Alignment.TopStart)
|
||||
) {
|
||||
CustomCheckbox(
|
||||
selected = file.isSelected,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
size = 18.dp,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
file.fileOrDirectory.name,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
lineHeight = 18.sp,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
"$fileSize, $lastModified",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout.grid
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BrowseGridItem(
|
||||
modifier: Modifier,
|
||||
file: SelectableFile,
|
||||
hasSelectedFiles: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onFavoriteClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(3.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
if (file.isSelected) MaterialTheme.colorScheme.secondaryContainer
|
||||
else Color.Transparent,
|
||||
RoundedCornerShape(12.dp)
|
||||
)
|
||||
.combinedClickable(
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick
|
||||
)
|
||||
.padding(5.dp)
|
||||
) {
|
||||
when {
|
||||
!file.isDirectory -> {
|
||||
BrowseGridFileItem(
|
||||
file = file,
|
||||
hasSelectedFiles = hasSelectedFiles
|
||||
)
|
||||
}
|
||||
|
||||
file.isDirectory -> {
|
||||
BrowseGridDirectoryItem(
|
||||
file = file,
|
||||
hasSelectedFiles = hasSelectedFiles,
|
||||
onFavoriteClick = onFavoriteClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout.grid
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.presentation.components.customItems
|
||||
import ua.acclorite.book_story.presentation.components.header
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.layout.BrowseItem
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout
|
||||
|
||||
@Composable
|
||||
fun BrowseGridLayout(
|
||||
state: State<BrowseState>,
|
||||
gridSize: Int,
|
||||
autoGridSize: Boolean,
|
||||
filteredFiles: List<SelectableFile>,
|
||||
onLongItemClick: (SelectableFile) -> Unit,
|
||||
onFavoriteItemClick: (SelectableFile) -> Unit,
|
||||
onItemClick: (SelectableFile) -> Unit,
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = if (autoGridSize) GridCells.Adaptive(170.dp)
|
||||
else GridCells.Fixed(gridSize.coerceAtLeast(1)),
|
||||
state = state.value.gridState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp)
|
||||
) {
|
||||
header {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
customItems(
|
||||
filteredFiles,
|
||||
key = { it.fileOrDirectory.path }
|
||||
) { selectableFile ->
|
||||
BrowseItem(
|
||||
file = selectableFile,
|
||||
modifier = Modifier.animateItem(),
|
||||
layout = BrowseLayout.GRID,
|
||||
hasSelectedFiles = state.value.selectableFiles.any { it.isSelected },
|
||||
onLongClick = {
|
||||
onLongItemClick(selectableFile)
|
||||
},
|
||||
onFavoriteClick = {
|
||||
onFavoriteItemClick(selectableFile)
|
||||
},
|
||||
onClick = {
|
||||
onItemClick(selectableFile)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
header {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout.list
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun RowScope.BrowseListDirectoryItem(file: SelectableFile) {
|
||||
val lastModified = rememberSaveable {
|
||||
SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
||||
.format(Date(file.fileOrDirectory.lastModified()))
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Folder,
|
||||
contentDescription = stringResource(id = R.string.directory_icon_content_desc),
|
||||
modifier = Modifier
|
||||
.padding(14.dp)
|
||||
.size(22.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
file.fileOrDirectory.name,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
lineHeight = 18.sp,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
lastModified,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout.list
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun RowScope.BrowseListFileItem(file: SelectableFile) {
|
||||
val lastModified = rememberSaveable {
|
||||
SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
|
||||
.format(Date(file.fileOrDirectory.lastModified()))
|
||||
}
|
||||
|
||||
val sizeBytes = rememberSaveable { file.fileOrDirectory.length() }
|
||||
val fileSizeKB = rememberSaveable {
|
||||
if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0
|
||||
}
|
||||
val fileSizeMB = rememberSaveable {
|
||||
if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0
|
||||
}
|
||||
val fileSize = rememberSaveable {
|
||||
if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB)
|
||||
else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB)
|
||||
else "0 KB"
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
1.dp,
|
||||
if (file.isSelected) MaterialTheme.colorScheme.outline
|
||||
else MaterialTheme.colorScheme.outlineVariant,
|
||||
RoundedCornerShape(6.dp)
|
||||
)
|
||||
.padding(14.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||
contentDescription = stringResource(id = R.string.file_icon_content_desc),
|
||||
modifier = Modifier
|
||||
.size(22.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
Text(
|
||||
file.fileOrDirectory.name,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
lineHeight = 18.sp,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
"$fileSize, $lastModified",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout.list
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||
import ua.acclorite.book_story.presentation.ui.FadeTransitionPreservingSpace
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BrowseListItem(
|
||||
modifier: Modifier,
|
||||
file: SelectableFile,
|
||||
hasSelectedFiles: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onFavoriteClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 3.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(
|
||||
if (file.isSelected) MaterialTheme.colorScheme.secondaryContainer
|
||||
else Color.Transparent,
|
||||
RoundedCornerShape(10.dp)
|
||||
)
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
},
|
||||
onClick = {
|
||||
onClick()
|
||||
}
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 7.dp)
|
||||
) {
|
||||
when {
|
||||
!file.isDirectory -> {
|
||||
BrowseListFileItem(file = file)
|
||||
|
||||
FadeTransitionPreservingSpace(
|
||||
visible = hasSelectedFiles,
|
||||
animationSpec = tween(300)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
CustomCheckbox(
|
||||
selected = file.isSelected,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
size = 18.dp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file.isDirectory -> {
|
||||
BrowseListDirectoryItem(file = file)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
FadeTransitionPreservingSpace(
|
||||
hasSelectedFiles,
|
||||
animationSpec = tween(300)
|
||||
) {
|
||||
CustomCheckbox(
|
||||
selected = file.isSelected,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
size = 18.dp
|
||||
)
|
||||
}
|
||||
FadeTransitionPreservingSpace(
|
||||
!hasSelectedFiles,
|
||||
animationSpec = tween(300)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (file.isFavorite) Icons.Default.Favorite
|
||||
else Icons.Default.FavoriteBorder,
|
||||
contentDescription = stringResource(
|
||||
id = R.string.favorite_directory_content_desc
|
||||
),
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable(
|
||||
enabled = !hasSelectedFiles,
|
||||
interactionSource = null,
|
||||
indication = null
|
||||
) {
|
||||
onFavoriteClick()
|
||||
},
|
||||
tint = if (file.isFavorite) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurfaceVariant.copy(0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.layout.list
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.presentation.components.customItems
|
||||
import ua.acclorite.book_story.presentation.screens.browse.components.layout.BrowseItem
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout
|
||||
|
||||
@Composable
|
||||
fun BrowseListLayout(
|
||||
state: State<BrowseState>,
|
||||
filteredFiles: List<SelectableFile>,
|
||||
onLongItemClick: (SelectableFile) -> Unit,
|
||||
onFavoriteItemClick: (SelectableFile) -> Unit,
|
||||
onItemClick: (SelectableFile) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
state = state.value.listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
customItems(
|
||||
filteredFiles,
|
||||
key = { it.fileOrDirectory.path }
|
||||
) { selectableFile ->
|
||||
BrowseItem(
|
||||
file = selectableFile,
|
||||
layout = BrowseLayout.LIST,
|
||||
modifier = Modifier.animateItem(),
|
||||
hasSelectedFiles = state.value.selectableFiles.any { it.isSelected },
|
||||
onLongClick = {
|
||||
onLongItemClick(selectableFile)
|
||||
},
|
||||
onFavoriteClick = {
|
||||
onFavoriteItemClick(selectableFile)
|
||||
},
|
||||
onClick = {
|
||||
onItemClick(selectableFile)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.top_bar
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.SelectAll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
||||
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
||||
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||
import ua.acclorite.book_story.presentation.components.CustomSearchTextField
|
||||
import ua.acclorite.book_story.presentation.components.MoreDropDown
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout
|
||||
|
||||
/**
|
||||
* Browse Top Bar.
|
||||
*
|
||||
* @param state [BrowseState].
|
||||
* @param mainState [MainState].
|
||||
* @param filteredFiles Filtered files.
|
||||
* @param onEvent [BrowseEvent] callback.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BrowseTopBar(
|
||||
state: State<BrowseState>,
|
||||
mainState: State<MainState>,
|
||||
filteredFiles: List<SelectableFile>,
|
||||
onEvent: (BrowseEvent) -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val selectedDirectoryName = remember {
|
||||
mutableStateOf(state.value.selectedDirectory.name)
|
||||
}
|
||||
val inNestedDirectory = remember {
|
||||
derivedStateOf {
|
||||
state.value.inNestedDirectory
|
||||
&& mainState.value.browseFilesStructure != BrowseFilesStructure.ALL_FILES
|
||||
}
|
||||
}
|
||||
val isScrolled = remember {
|
||||
derivedStateOf {
|
||||
when (mainState.value.browseLayout!!) {
|
||||
BrowseLayout.LIST -> state.value.listState.canScrollBackward
|
||||
BrowseLayout.GRID -> state.value.gridState.canScrollBackward
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
state.value.inNestedDirectory,
|
||||
state.value.selectedDirectory
|
||||
) {
|
||||
if (state.value.inNestedDirectory) {
|
||||
selectedDirectoryName.value = state.value.selectedDirectory.name
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedTopAppBar(
|
||||
scrollBehavior = null,
|
||||
isTopBarScrolled = state.value.hasSelectedItems ||
|
||||
isScrolled.value ||
|
||||
inNestedDirectory.value,
|
||||
|
||||
content1Visibility = !state.value.hasSelectedItems && !state.value.showSearch,
|
||||
content1NavigationIcon = {
|
||||
CustomAnimatedVisibility(
|
||||
visible = inNestedDirectory.value,
|
||||
enter = expandHorizontally(tween(200))
|
||||
+ slideInHorizontally(tween(200))
|
||||
+ fadeIn(tween(250)),
|
||||
exit = shrinkHorizontally(tween(150))
|
||||
+ slideOutHorizontally(tween(150))
|
||||
+ fadeOut(tween(200))
|
||||
) {
|
||||
CustomIconButton(
|
||||
icon = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = R.string.go_back_content_desc,
|
||||
disableOnClick = false,
|
||||
enabled = inNestedDirectory.value
|
||||
) {
|
||||
onEvent(BrowseEvent.OnGoBackDirectory)
|
||||
}
|
||||
}
|
||||
},
|
||||
content1Title = {
|
||||
Crossfade(
|
||||
targetState = inNestedDirectory.value,
|
||||
animationSpec = tween(200),
|
||||
label = ""
|
||||
) {
|
||||
if (it) {
|
||||
Text(
|
||||
selectedDirectoryName.value,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(id = R.string.browse_screen),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content1Actions = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Search,
|
||||
contentDescription = R.string.search_content_desc,
|
||||
disableOnClick = true
|
||||
) {
|
||||
onEvent(BrowseEvent.OnSearchShowHide)
|
||||
}
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.FilterList,
|
||||
contentDescription = R.string.filter_content_desc,
|
||||
disableOnClick = false,
|
||||
color = animateColorAsState(
|
||||
if (mainState.value.browseIncludedFilterItems!!.isNotEmpty()) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else LocalContentColor.current,
|
||||
label = ""
|
||||
).value,
|
||||
enabled = !state.value.showFilterBottomSheet
|
||||
) {
|
||||
onEvent(BrowseEvent.OnShowHideFilterBottomSheet)
|
||||
}
|
||||
MoreDropDown()
|
||||
},
|
||||
|
||||
content2Visibility = state.value.hasSelectedItems,
|
||||
content2NavigationIcon = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Clear,
|
||||
contentDescription = R.string.clear_selected_items_content_desc,
|
||||
disableOnClick = true
|
||||
) {
|
||||
onEvent(BrowseEvent.OnClearSelectedFiles)
|
||||
}
|
||||
},
|
||||
content2Title = {
|
||||
Text(
|
||||
stringResource(
|
||||
id = R.string.selected_items_count_query,
|
||||
state.value.selectedItemsCount.coerceAtLeast(1)
|
||||
),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
},
|
||||
content2Actions = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.SelectAll,
|
||||
contentDescription = R.string.select_all_files_content_desc,
|
||||
disableOnClick = false,
|
||||
) {
|
||||
onEvent(
|
||||
BrowseEvent.OnSelectFiles(
|
||||
includedFileFormats = mainState.value.browseIncludedFilterItems!!,
|
||||
files = filteredFiles
|
||||
)
|
||||
)
|
||||
}
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Check,
|
||||
contentDescription = R.string.add_files_content_desc,
|
||||
disableOnClick = false,
|
||||
enabled = !state.value.showAddingDialog
|
||||
) {
|
||||
onEvent(BrowseEvent.OnAddingDialogRequest)
|
||||
}
|
||||
},
|
||||
|
||||
content3Visibility = state.value.showSearch && !state.value.hasSelectedItems,
|
||||
content3NavigationIcon = {
|
||||
CustomIconButton(
|
||||
icon = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = R.string.exit_search_content_desc,
|
||||
disableOnClick = true
|
||||
) {
|
||||
onEvent(BrowseEvent.OnSearchShowHide)
|
||||
}
|
||||
},
|
||||
content3Title = {
|
||||
CustomSearchTextField(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.onGloballyPositioned {
|
||||
onEvent(BrowseEvent.OnRequestFocus(focusRequester))
|
||||
},
|
||||
query = state.value.searchQuery,
|
||||
onQueryChange = {
|
||||
onEvent(BrowseEvent.OnSearchQueryChange(it))
|
||||
},
|
||||
onSearch = {
|
||||
onEvent(BrowseEvent.OnSearch)
|
||||
},
|
||||
placeholder = stringResource(
|
||||
id = R.string.search_query,
|
||||
stringResource(id = R.string.files)
|
||||
)
|
||||
)
|
||||
},
|
||||
content3Actions = {
|
||||
MoreDropDown()
|
||||
},
|
||||
|
||||
customContent = {
|
||||
BrowseTopBarDirectoryPath(
|
||||
state = state,
|
||||
mainState = mainState,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.components.top_bar
|
||||
|
||||
import android.os.Environment
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowRight
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
||||
import ua.acclorite.book_story.presentation.components.customItemsIndexed
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent
|
||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun BrowseTopBarDirectoryPath(
|
||||
state: State<BrowseState>,
|
||||
mainState: State<MainState>,
|
||||
onEvent: (BrowseEvent) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val rootDirectory = remember {
|
||||
Environment.getExternalStorageDirectory()
|
||||
}
|
||||
val directories = remember(state.value.selectedDirectory) {
|
||||
val directories = mutableListOf<Pair<String, File>>()
|
||||
|
||||
if (state.value.selectedDirectory == rootDirectory) {
|
||||
return@remember listOf(
|
||||
context.getString(R.string.internal_storage) to rootDirectory
|
||||
)
|
||||
}
|
||||
|
||||
directories.add(state.value.selectedDirectory.name to state.value.selectedDirectory)
|
||||
var currentDirectory = state.value.selectedDirectory.parentFile
|
||||
?: return@remember emptyList()
|
||||
|
||||
while (true) {
|
||||
if (currentDirectory == rootDirectory) {
|
||||
break
|
||||
}
|
||||
|
||||
directories.add(
|
||||
currentDirectory.name to currentDirectory
|
||||
)
|
||||
currentDirectory = currentDirectory.parentFile ?: continue
|
||||
}
|
||||
|
||||
directories.add(
|
||||
context.getString(R.string.internal_storage) to rootDirectory
|
||||
)
|
||||
|
||||
directories.reversed()
|
||||
}
|
||||
val listState = rememberLazyListState(directories.lastIndex)
|
||||
|
||||
LaunchedEffect(directories) {
|
||||
try {
|
||||
listState.scrollToItem(directories.lastIndex)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
CustomAnimatedVisibility(
|
||||
visible = !state.value.hasSelectedItems
|
||||
&& !state.value.hasSearched
|
||||
&& !state.value.isError
|
||||
&& mainState.value.browseFilesStructure == BrowseFilesStructure.DIRECTORIES,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically()
|
||||
) {
|
||||
LazyRow(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp),
|
||||
state = listState,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
customItemsIndexed(directories, key = { _, index -> index }) { index, directory ->
|
||||
if (index == 0) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = directory.first,
|
||||
color = animateColorAsState(
|
||||
if (index == directories.lastIndex) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
label = ""
|
||||
).value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.clickable(
|
||||
enabled = state.value.selectedDirectory != directory.second,
|
||||
indication = null,
|
||||
interactionSource = null
|
||||
) {
|
||||
onEvent(
|
||||
BrowseEvent.OnChangeDirectory(
|
||||
directory.second,
|
||||
savePreviousDirectory = false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (index < directories.lastIndex) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.ArrowRight,
|
||||
contentDescription = stringResource(id = R.string.path_arrow_icon_content_desc),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,13 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.data
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.domain.util.OnNavigate
|
||||
import ua.acclorite.book_story.domain.util.Selected
|
||||
import java.io.File
|
||||
|
|
@ -16,22 +18,28 @@ import java.io.File
|
|||
sealed class BrowseEvent {
|
||||
data class OnStoragePermissionRequest(
|
||||
val activity: ComponentActivity,
|
||||
val storagePermissionState: PermissionState,
|
||||
val hideErrorMessage: () -> Unit
|
||||
val storagePermissionState: PermissionState
|
||||
) : BrowseEvent()
|
||||
|
||||
data class OnStoragePermissionDismiss(
|
||||
val permissionState: PermissionState,
|
||||
val showErrorMessage: () -> Unit
|
||||
val permissionState: PermissionState
|
||||
) : BrowseEvent()
|
||||
|
||||
data object OnRefreshList : BrowseEvent()
|
||||
data class OnPermissionCheck(
|
||||
val permissionState: PermissionState,
|
||||
val hideErrorMessage: () -> Unit
|
||||
val permissionState: PermissionState
|
||||
) : BrowseEvent()
|
||||
|
||||
data class OnSelectFile(
|
||||
val includedFileFormats: List<String>,
|
||||
val file: SelectableFile
|
||||
) : BrowseEvent()
|
||||
|
||||
data class OnSelectFiles(
|
||||
val includedFileFormats: List<String>,
|
||||
val files: List<SelectableFile>
|
||||
) : BrowseEvent()
|
||||
|
||||
data class OnSelectFile(val file: Pair<File, Selected>) : BrowseEvent()
|
||||
data class OnSelectBook(val book: Pair<NullableBook, Selected>) : BrowseEvent()
|
||||
data object OnSearchShowHide : BrowseEvent()
|
||||
data class OnRequestFocus(val focusRequester: FocusRequester) : BrowseEvent()
|
||||
|
|
@ -50,4 +58,11 @@ sealed class BrowseEvent {
|
|||
|
||||
data object OnLoadList : BrowseEvent()
|
||||
data object OnUpdateScrollOffset : BrowseEvent()
|
||||
data class OnChangeDirectory(val directory: File, val savePreviousDirectory: Boolean) :
|
||||
BrowseEvent()
|
||||
|
||||
data object OnGoBackDirectory : BrowseEvent()
|
||||
data object OnShowHideFilterBottomSheet : BrowseEvent()
|
||||
data class OnScrollToFilterPage(val page: Int, val pagerState: PagerState?) : BrowseEvent()
|
||||
data class OnUpdateFavoriteDirectory(val path: String) : BrowseEvent()
|
||||
}
|
||||
|
|
@ -1,18 +1,29 @@
|
|||
package ua.acclorite.book_story.presentation.screens.browse.data
|
||||
|
||||
import android.os.Environment
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.domain.util.Selected
|
||||
import java.io.File
|
||||
|
||||
@Immutable
|
||||
data class BrowseState(
|
||||
val selectableFiles: List<Pair<File, Selected>> = emptyList(),
|
||||
val listState: LazyListState = LazyListState(0, 0),
|
||||
val selectableFiles: List<SelectableFile> = emptyList(),
|
||||
val listState: LazyListState = LazyListState(),
|
||||
val gridState: LazyGridState = LazyGridState(),
|
||||
|
||||
val selectedDirectory: File = Environment.getExternalStorageDirectory(),
|
||||
val previousDirectory: File? = null,
|
||||
val inNestedDirectory: Boolean = false,
|
||||
val showFilterBottomSheet: Boolean = false,
|
||||
val currentPage: Int = 1,
|
||||
|
||||
val isLoading: Boolean = true,
|
||||
val isRefreshing: Boolean = false,
|
||||
val isError: Boolean = false,
|
||||
val requestPermissionDialog: Boolean = false,
|
||||
|
||||
val selectedItemsCount: Int = 0,
|
||||
|
|
@ -20,6 +31,7 @@ data class BrowseState(
|
|||
|
||||
val showSearch: Boolean = false,
|
||||
val searchQuery: String = "",
|
||||
val hasSearched: Boolean = false,
|
||||
val hasFocused: Boolean = false,
|
||||
|
||||
val showAddingDialog: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("LABEL_NAME_CLASH")
|
||||
|
||||
package ua.acclorite.book_story.presentation.screens.browse.data
|
||||
|
||||
import android.content.Intent
|
||||
|
|
@ -6,6 +8,7 @@ import android.os.Build
|
|||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
|
@ -21,35 +24,43 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles
|
||||
import ua.acclorite.book_story.domain.model.SelectableFile
|
||||
import ua.acclorite.book_story.domain.use_case.GetBookFromFile
|
||||
import ua.acclorite.book_story.domain.use_case.GetFilesFromDevice
|
||||
import ua.acclorite.book_story.domain.use_case.InsertBook
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import ua.acclorite.book_story.domain.use_case.UpdateFavoriteDirectory
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.data.Screen
|
||||
import ua.acclorite.book_story.presentation.data.launchActivity
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseSortOrder
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@HiltViewModel
|
||||
class BrowseViewModel @Inject constructor(
|
||||
private val getBooksFromFiles: GetBooksFromFiles,
|
||||
private val getBookFromFile: GetBookFromFile,
|
||||
private val getFilesFromDevice: GetFilesFromDevice,
|
||||
private val insertBook: InsertBook
|
||||
private val insertBook: InsertBook,
|
||||
private val updateFavoriteDirectory: UpdateFavoriteDirectory,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(BrowseState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var job: Job? = null
|
||||
private var job2: Job? = null
|
||||
private var job3: Job? = null
|
||||
private var searchQueryJob: Job? = null
|
||||
private var refreshListJob: Job? = null
|
||||
private var getBooksJob: Job? = null
|
||||
private var storagePermissionJob: Job? = null
|
||||
private var changeDirectoryJob: Job? = null
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = true
|
||||
isLoading = true,
|
||||
selectedDirectory = Environment.getExternalStorageDirectory(),
|
||||
inNestedDirectory = false
|
||||
)
|
||||
}
|
||||
getFilesFromDownloads()
|
||||
|
|
@ -70,10 +81,10 @@ class BrowseViewModel @Inject constructor(
|
|||
if (isPermissionGranted) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
requestPermissionDialog = false
|
||||
requestPermissionDialog = false,
|
||||
isError = false
|
||||
)
|
||||
}
|
||||
event.hideErrorMessage()
|
||||
onEvent(BrowseEvent.OnRefreshList)
|
||||
return
|
||||
}
|
||||
|
|
@ -130,10 +141,10 @@ class BrowseViewModel @Inject constructor(
|
|||
|
||||
_state.update {
|
||||
it.copy(
|
||||
requestPermissionDialog = false
|
||||
requestPermissionDialog = false,
|
||||
isError = false
|
||||
)
|
||||
}
|
||||
event.hideErrorMessage()
|
||||
onEvent(BrowseEvent.OnRefreshList)
|
||||
break
|
||||
}
|
||||
|
|
@ -158,19 +169,24 @@ class BrowseViewModel @Inject constructor(
|
|||
getFilesFromDownloads()
|
||||
}
|
||||
} else {
|
||||
event.showErrorMessage()
|
||||
_state.update {
|
||||
it.copy(
|
||||
isError = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnRefreshList -> {
|
||||
job2?.cancel()
|
||||
job2 = viewModelScope.launch(Dispatchers.IO) {
|
||||
refreshListJob?.cancel()
|
||||
refreshListJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isRefreshing = true,
|
||||
hasSelectedItems = false,
|
||||
showSearch = false,
|
||||
listState = LazyListState(0, 0)
|
||||
listState = LazyListState(),
|
||||
gridState = LazyGridState(),
|
||||
)
|
||||
}
|
||||
yield()
|
||||
|
|
@ -197,30 +213,94 @@ class BrowseViewModel @Inject constructor(
|
|||
|
||||
_state.update {
|
||||
it.copy(
|
||||
requestPermissionDialog = true
|
||||
requestPermissionDialog = true,
|
||||
isError = false
|
||||
)
|
||||
}
|
||||
event.hideErrorMessage()
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnSelectFile -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val editedList = _state.value.selectableFiles.map {
|
||||
if (event.file.first.path == it.first.path) {
|
||||
it.copy(
|
||||
second = !it.second
|
||||
)
|
||||
} else {
|
||||
it
|
||||
val editedList = _state.value.selectableFiles.map { file ->
|
||||
when (event.file.isDirectory) {
|
||||
false -> {
|
||||
if (event.file.fileOrDirectory.path == file.fileOrDirectory.path) {
|
||||
file.copy(
|
||||
isSelected = !file.isSelected
|
||||
)
|
||||
} else {
|
||||
file
|
||||
}
|
||||
}
|
||||
|
||||
true -> {
|
||||
if (
|
||||
file.fileOrDirectory.path.startsWith(
|
||||
event.file.fileOrDirectory.path
|
||||
) && event.includedFileFormats.run {
|
||||
if (isEmpty()) return@run true
|
||||
any {
|
||||
file.fileOrDirectory.path.endsWith(
|
||||
it, ignoreCase = true
|
||||
) || file.isDirectory
|
||||
}
|
||||
}
|
||||
) {
|
||||
file.copy(
|
||||
isSelected = !event.file.isSelected
|
||||
)
|
||||
} else {
|
||||
file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectableFiles = editedList,
|
||||
selectedItemsCount = editedList.filter { file -> file.second }.size,
|
||||
hasSelectedItems = editedList.any { file -> file.second }
|
||||
selectedItemsCount = editedList.filter { file ->
|
||||
file.isSelected && !file.isDirectory
|
||||
}.size,
|
||||
hasSelectedItems = editedList.any { file ->
|
||||
file.isSelected && !file.isDirectory
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnSelectFiles -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val editedList = _state.value.selectableFiles.map { file ->
|
||||
if (
|
||||
event.files.any {
|
||||
file.fileOrDirectory.path.startsWith(it.fileOrDirectory.path)
|
||||
} && event.includedFileFormats.run {
|
||||
if (isEmpty()) return@run true
|
||||
any {
|
||||
file.fileOrDirectory.path.endsWith(
|
||||
it, ignoreCase = true
|
||||
) || file.isDirectory
|
||||
}
|
||||
}
|
||||
) {
|
||||
file.copy(isSelected = true)
|
||||
} else {
|
||||
file
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectableFiles = editedList,
|
||||
selectedItemsCount = editedList.filter { file ->
|
||||
file.isSelected && !file.isDirectory
|
||||
}.size,
|
||||
hasSelectedItems = editedList.any { file ->
|
||||
file.isSelected && !file.isDirectory
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -262,6 +342,7 @@ class BrowseViewModel @Inject constructor(
|
|||
_state.update {
|
||||
it.copy(
|
||||
searchQuery = "",
|
||||
hasSearched = false,
|
||||
hasFocused = false
|
||||
)
|
||||
}
|
||||
|
|
@ -289,7 +370,11 @@ class BrowseViewModel @Inject constructor(
|
|||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectableFiles = it.selectableFiles.map { file -> file.copy(second = false) },
|
||||
selectableFiles = it.selectableFiles.map { file ->
|
||||
file.copy(
|
||||
isSelected = false
|
||||
)
|
||||
},
|
||||
hasSelectedItems = false
|
||||
)
|
||||
}
|
||||
|
|
@ -302,8 +387,8 @@ class BrowseViewModel @Inject constructor(
|
|||
searchQuery = event.query
|
||||
)
|
||||
}
|
||||
job?.cancel()
|
||||
job = viewModelScope.launch(Dispatchers.IO) {
|
||||
searchQueryJob?.cancel()
|
||||
searchQueryJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(500)
|
||||
yield()
|
||||
onEvent(BrowseEvent.OnSearch)
|
||||
|
|
@ -322,7 +407,7 @@ class BrowseViewModel @Inject constructor(
|
|||
showAddingDialog = false
|
||||
)
|
||||
}
|
||||
job3?.cancel(null)
|
||||
getBooksJob?.cancel()
|
||||
}
|
||||
|
||||
is BrowseEvent.OnAddingDialogRequest -> {
|
||||
|
|
@ -336,7 +421,7 @@ class BrowseViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
is BrowseEvent.OnGetBooksFromFiles -> {
|
||||
job3 = viewModelScope.launch(Dispatchers.IO) {
|
||||
getBooksJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isBooksLoading = true
|
||||
|
|
@ -347,15 +432,11 @@ class BrowseViewModel @Inject constructor(
|
|||
|
||||
val books = mutableListOf<NullableBook>()
|
||||
_state.value.selectableFiles
|
||||
.filter { it.second }
|
||||
.map { it.first }
|
||||
.filter { it.isSelected && !it.isDirectory }
|
||||
.map { it.fileOrDirectory }
|
||||
.forEach {
|
||||
yield()
|
||||
books.add(
|
||||
getBooksFromFiles.execute(
|
||||
listOf(it)
|
||||
).first()
|
||||
)
|
||||
books.add(getBookFromFile.execute(it))
|
||||
}
|
||||
|
||||
yield()
|
||||
|
|
@ -422,7 +503,8 @@ class BrowseViewModel @Inject constructor(
|
|||
it.copy(
|
||||
isLoading = true,
|
||||
hasSelectedItems = false,
|
||||
listState = LazyListState(0, 0)
|
||||
listState = LazyListState(),
|
||||
gridState = LazyGridState()
|
||||
)
|
||||
}
|
||||
getFilesFromDownloads()
|
||||
|
|
@ -435,29 +517,201 @@ class BrowseViewModel @Inject constructor(
|
|||
listState = LazyListState(
|
||||
it.listState.firstVisibleItemIndex,
|
||||
0
|
||||
),
|
||||
gridState = LazyGridState(
|
||||
it.gridState.firstVisibleItemIndex,
|
||||
0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnChangeDirectory -> {
|
||||
viewModelScope.launch {
|
||||
changeDirectoryJob?.cancel()
|
||||
searchQueryJob?.cancel()
|
||||
|
||||
changeDirectoryJob = launch(Dispatchers.IO) {
|
||||
yield()
|
||||
_state.update {
|
||||
it.copy(
|
||||
listState = LazyListState(),
|
||||
gridState = LazyGridState(),
|
||||
selectedDirectory = event.directory,
|
||||
previousDirectory = if (event.savePreviousDirectory) it.selectedDirectory
|
||||
else event.directory.parentFile,
|
||||
inNestedDirectory = event.directory != Environment.getExternalStorageDirectory()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnGoBackDirectory -> {
|
||||
onEvent(
|
||||
BrowseEvent.OnChangeDirectory(
|
||||
_state.value.previousDirectory
|
||||
?: Environment.getExternalStorageDirectory(),
|
||||
savePreviousDirectory = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is BrowseEvent.OnShowHideFilterBottomSheet -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showFilterBottomSheet = !it.showFilterBottomSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnScrollToFilterPage -> {
|
||||
viewModelScope.launch {
|
||||
_state.update {
|
||||
event.pagerState?.scrollToPage(event.page)
|
||||
|
||||
it.copy(
|
||||
currentPage = event.page
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnUpdateFavoriteDirectory -> {
|
||||
viewModelScope.launch {
|
||||
_state.update {
|
||||
it.copy(isLoading = true)
|
||||
}
|
||||
updateFavoriteDirectory.execute(event.path)
|
||||
getFilesFromDownloads()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filterList(mainState: MainState): List<SelectableFile> {
|
||||
fun <T> thenCompareBy(
|
||||
selector: (T) -> Comparable<*>?
|
||||
): Comparator<T> {
|
||||
return if (mainState.browseSortOrderDescending!!) {
|
||||
compareByDescending(selector)
|
||||
} else {
|
||||
compareBy(selector)
|
||||
}
|
||||
}
|
||||
|
||||
fun List<SelectableFile>.filterFiles(): List<SelectableFile> {
|
||||
if (mainState.browseIncludedFilterItems!!.isEmpty()) {
|
||||
return this
|
||||
}
|
||||
|
||||
return filter { file ->
|
||||
when (file.isDirectory) {
|
||||
true -> {
|
||||
return@filter this.filter {
|
||||
if (file == it) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
it.fileOrDirectory.path.startsWith(file.fileOrDirectory.path)
|
||||
}.filterFiles().isNotEmpty()
|
||||
}
|
||||
|
||||
false -> {
|
||||
return@filter mainState.browseIncludedFilterItems.any {
|
||||
file.fileOrDirectory.path.endsWith(
|
||||
it, ignoreCase = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _state.value.selectableFiles
|
||||
.filterFiles()
|
||||
.filter {
|
||||
if (
|
||||
_state.value.hasSearched
|
||||
|| mainState.browseFilesStructure == BrowseFilesStructure.ALL_FILES
|
||||
) {
|
||||
return@filter !it.isDirectory
|
||||
}
|
||||
|
||||
if (
|
||||
Environment.getExternalStorageDirectory() == _state.value.selectedDirectory
|
||||
&& it.isFavorite
|
||||
&& mainState.browsePinFavoriteDirectories!!
|
||||
) {
|
||||
return@filter true
|
||||
}
|
||||
|
||||
it.parentDirectory == _state.value.selectedDirectory
|
||||
}
|
||||
.sortedWith(
|
||||
compareByDescending<SelectableFile> {
|
||||
when (mainState.browsePinFavoriteDirectories!!) {
|
||||
true -> it.isFavorite
|
||||
false -> true
|
||||
}
|
||||
}.then(
|
||||
compareByDescending {
|
||||
when (mainState.browseSortOrder!! != BrowseSortOrder.FILE_TYPE) {
|
||||
true -> it.isDirectory
|
||||
false -> true
|
||||
}
|
||||
}
|
||||
).then(
|
||||
thenCompareBy {
|
||||
when (mainState.browseSortOrder!!) {
|
||||
BrowseSortOrder.NAME -> {
|
||||
it.fileOrDirectory.name.lowercase().trim()
|
||||
}
|
||||
|
||||
BrowseSortOrder.FILE_TYPE -> {
|
||||
it.isDirectory
|
||||
}
|
||||
|
||||
BrowseSortOrder.FILE_FORMAT -> {
|
||||
it.fileOrDirectory.extension
|
||||
}
|
||||
|
||||
BrowseSortOrder.FILE_SIZE -> {
|
||||
it.fileOrDirectory.length()
|
||||
}
|
||||
|
||||
BrowseSortOrder.LAST_MODIFIED -> {
|
||||
it.fileOrDirectory.lastModified()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getFilesFromDownloads(
|
||||
query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
|
||||
) {
|
||||
getFilesFromDevice.execute(query).collect { result ->
|
||||
when (result) {
|
||||
is Resource.Success -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectableFiles = result.data?.map { book -> Pair(book, false) }
|
||||
?: emptyList(),
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
getFilesFromDevice.execute(query).apply {
|
||||
yield()
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectableFiles = this,
|
||||
selectedItemsCount = 0,
|
||||
hasSearched = query.isNotBlank(),
|
||||
hasSelectedItems = false,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Resource.Error -> Unit
|
||||
fun clearViewModel() {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
_state.update {
|
||||
it.copy(isError = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuBook
|
||||
import androidx.compose.material.icons.filled.DisplaySettings
|
||||
import androidx.compose.material.icons.filled.Explore
|
||||
import androidx.compose.material.icons.filled.Palette
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
|
|
@ -113,6 +114,18 @@ private fun SettingsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsCategoryItem(
|
||||
icon = Icons.Default.Explore,
|
||||
text = stringResource(id = R.string.browse_settings),
|
||||
description = stringResource(id = R.string.browse_settings_desc)
|
||||
) {
|
||||
onNavigate {
|
||||
navigate(Screen.Settings.BrowseSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(48.dp)) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.util.OnNavigate
|
||||
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||
import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.BrowseSettingsCategory
|
||||
|
||||
@Composable
|
||||
fun BrowseSettingsRoot() {
|
||||
val navigator = LocalNavigator.current
|
||||
val mainViewModel: MainViewModel = hiltViewModel()
|
||||
|
||||
val state = mainViewModel.state.collectAsState()
|
||||
|
||||
BrowseSettings(
|
||||
state = state,
|
||||
onNavigate = { navigator.it() },
|
||||
onMainEvent = mainViewModel::onEvent
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BrowseSettings(
|
||||
state: State<MainState>,
|
||||
onNavigate: OnNavigate,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
|
||||
|
||||
Scaffold(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollState.first.nestedScrollConnection)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.imePadding(),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
topBar = {
|
||||
LargeTopAppBar(
|
||||
title = {
|
||||
Text(stringResource(id = R.string.browse_settings))
|
||||
},
|
||||
navigationIcon = {
|
||||
GoBackButton(onNavigate = onNavigate)
|
||||
},
|
||||
scrollBehavior = scrollState.first,
|
||||
colors = TopAppBarDefaults.largeTopAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = paddingValues.calculateTopPadding()),
|
||||
state = scrollState.second
|
||||
) {
|
||||
BrowseSettingsCategory(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
@file:Suppress("FunctionName")
|
||||
|
||||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseFilterSubcategory
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseGeneralSubcategory
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories.BrowseSortSubcategory
|
||||
|
||||
/**
|
||||
* Browse Settings Category.
|
||||
* Contains all Browse settings.
|
||||
*
|
||||
* @param state [MainState] instance.
|
||||
* @param onMainEvent [MainEvent] callback.
|
||||
* @param topPadding Top padding to be applied.
|
||||
* @param bottomPadding Bottom padding to be applied.
|
||||
*/
|
||||
fun LazyListScope.BrowseSettingsCategory(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit,
|
||||
topPadding: Dp = 16.dp,
|
||||
bottomPadding: Dp = 48.dp
|
||||
) {
|
||||
BrowseGeneralSubcategory(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent,
|
||||
topPadding = topPadding,
|
||||
bottomPadding = 0.dp
|
||||
)
|
||||
BrowseFilterSubcategory(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent,
|
||||
topPadding = 22.dp,
|
||||
bottomPadding = 0.dp
|
||||
)
|
||||
BrowseSortSubcategory(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent,
|
||||
topPadding = 22.dp,
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.ButtonItem
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.SegmentedButtonWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure
|
||||
|
||||
/**
|
||||
* Browse Files Structure setting.
|
||||
* Lets user choose between all and folders structure for Browse.
|
||||
*/
|
||||
@Composable
|
||||
fun LazyItemScope.BrowseFilesStructureSetting(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
SegmentedButtonWithTitle(
|
||||
title = stringResource(id = R.string.files_structure_option),
|
||||
modifier = Modifier.animateItem(),
|
||||
buttons = BrowseFilesStructure.entries.map {
|
||||
ButtonItem(
|
||||
it.toString(),
|
||||
when (it) {
|
||||
BrowseFilesStructure.ALL_FILES -> stringResource(id = R.string.files_structure_all)
|
||||
BrowseFilesStructure.DIRECTORIES -> stringResource(id = R.string.files_structure_directory)
|
||||
},
|
||||
MaterialTheme.typography.labelLarge,
|
||||
it == state.value.browseFilesStructure
|
||||
)
|
||||
}
|
||||
) {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeBrowseFilesStructure(
|
||||
it.id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
@file:Suppress("FunctionName")
|
||||
|
||||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
import ua.acclorite.book_story.presentation.components.customItems
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
|
||||
/**
|
||||
* Browse Filter setting.
|
||||
* Lets user choose which items to filter in Browse.
|
||||
*/
|
||||
fun LazyListScope.BrowseFilterSetting(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
customItems(Constants.EXTENSIONS, key = { it }) {
|
||||
FilterItem(
|
||||
item = it,
|
||||
isSelected = state.value.browseIncludedFilterItems!!.any { item ->
|
||||
item == it
|
||||
}
|
||||
) {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeBrowseIncludedFilterItem(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter Item.
|
||||
* Can be selected.
|
||||
*
|
||||
* @param item Item.
|
||||
* @param isSelected Whether item is selected.
|
||||
* @param onClick OnClick callback.
|
||||
*/
|
||||
@Composable
|
||||
private fun LazyItemScope.FilterItem(
|
||||
item: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
.padding(horizontal = 14.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onClick() }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(14.dp))
|
||||
|
||||
Text(
|
||||
text = item,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout
|
||||
import ua.acclorite.book_story.presentation.ui.SlidingTransition
|
||||
|
||||
/**
|
||||
* Browse Grid Size setting.
|
||||
* Lets user specify grid size to be displayed.
|
||||
*/
|
||||
@Composable
|
||||
fun LazyItemScope.BrowseGridSizeSetting(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
SlidingTransition(
|
||||
modifier = Modifier.animateItem(
|
||||
fadeInSpec = null,
|
||||
fadeOutSpec = null
|
||||
),
|
||||
visible = state.value.browseLayout == BrowseLayout.GRID,
|
||||
) {
|
||||
SliderWithTitle(
|
||||
value = state.value.browseGridSize!!
|
||||
to " ${stringResource(R.string.browse_grid_size_per_row)}",
|
||||
valuePlaceholder = stringResource(id = R.string.browse_grid_size_auto),
|
||||
showPlaceholder = state.value.browseAutoGridSize!!,
|
||||
modifier = Modifier.animateItem(),
|
||||
fromValue = 0,
|
||||
toValue = 10,
|
||||
title = stringResource(id = R.string.browse_grid_size_option),
|
||||
onValueChange = {
|
||||
onMainEvent(MainEvent.OnChangeBrowseAutoGridSize(it == 0))
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeBrowseGridSize(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.ButtonItem
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.SegmentedButtonWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseLayout
|
||||
|
||||
/**
|
||||
* Browse Layout setting.
|
||||
* Lets user choose between layouts(list, grid).
|
||||
*/
|
||||
@Composable
|
||||
fun LazyItemScope.BrowseLayoutSetting(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
SegmentedButtonWithTitle(
|
||||
title = stringResource(id = R.string.browse_layout_option),
|
||||
modifier = Modifier.animateItem(),
|
||||
buttons = BrowseLayout.entries.map {
|
||||
ButtonItem(
|
||||
it.toString(),
|
||||
when (it) {
|
||||
BrowseLayout.LIST -> stringResource(id = R.string.browse_layout_list)
|
||||
BrowseLayout.GRID -> stringResource(id = R.string.browse_layout_grid)
|
||||
},
|
||||
MaterialTheme.typography.labelLarge,
|
||||
it == state.value.browseLayout
|
||||
)
|
||||
}
|
||||
) {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeBrowseLayout(
|
||||
it.id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.SwitchWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseFilesStructure
|
||||
import ua.acclorite.book_story.presentation.ui.SlidingTransition
|
||||
|
||||
/**
|
||||
* Browse Pin Favorite Directories setting.
|
||||
* If true, shows all favorite directories in root directory and moves them to the top in nested directories.
|
||||
*/
|
||||
@Composable
|
||||
fun LazyItemScope.BrowsePinFavoriteDirectoriesSetting(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
SlidingTransition(
|
||||
modifier = Modifier.animateItem(
|
||||
fadeInSpec = null,
|
||||
fadeOutSpec = null
|
||||
),
|
||||
visible = state.value.browseFilesStructure == BrowseFilesStructure.DIRECTORIES
|
||||
) {
|
||||
SwitchWithTitle(
|
||||
selected = state.value.browsePinFavoriteDirectories!!,
|
||||
title = stringResource(id = R.string.browse_pin_favorite_directories_option),
|
||||
description = stringResource(id = R.string.browse_pin_favorite_directories_option_desc)
|
||||
) {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeBrowsePinFavoriteDirectories(
|
||||
!state.value.browsePinFavoriteDirectories!!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
@file:Suppress("FunctionName")
|
||||
|
||||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.customItems
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.data.BrowseSortOrder
|
||||
|
||||
/**
|
||||
* Browse Sort Order setting.
|
||||
* Lets user choose how to sort files in Browse.
|
||||
*/
|
||||
fun LazyListScope.BrowseSortOrderSetting(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
customItems(BrowseSortOrder.entries, key = { it.name }) {
|
||||
SortItem(
|
||||
item = it,
|
||||
isSelected = state.value.browseSortOrder!! == it,
|
||||
isDescending = state.value.browseSortOrderDescending!!
|
||||
) {
|
||||
if (state.value.browseSortOrder!! == it) {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeBrowseSortOrderDescending(
|
||||
!state.value.browseSortOrderDescending!!
|
||||
)
|
||||
)
|
||||
} else {
|
||||
onMainEvent(MainEvent.OnChangeBrowseSortOrderDescending(true))
|
||||
onMainEvent(MainEvent.OnChangeBrowseSortOrder(it.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort Item.
|
||||
* Can sort items by descending or ascending.
|
||||
*
|
||||
* @param item [BrowseSortOrder].
|
||||
* @param isSelected Whether items are currently sorted by this item.
|
||||
* @param isDescending Whether sorted by descending.
|
||||
* @param onClick OnClick callback.
|
||||
*/
|
||||
@Composable
|
||||
private fun LazyItemScope.SortItem(
|
||||
item: BrowseSortOrder,
|
||||
isSelected: Boolean,
|
||||
isDescending: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isDescending) Icons.Default.ArrowDownward
|
||||
else Icons.Default.ArrowUpward,
|
||||
contentDescription = stringResource(id = R.string.sort_order_content_desc),
|
||||
modifier = Modifier
|
||||
.size(28.dp),
|
||||
tint = if (isSelected) MaterialTheme.colorScheme.primary
|
||||
else Color.Transparent
|
||||
)
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(
|
||||
when (item) {
|
||||
BrowseSortOrder.NAME -> R.string.browse_sort_order_name
|
||||
BrowseSortOrder.FILE_FORMAT -> R.string.browse_sort_order_file_format
|
||||
BrowseSortOrder.FILE_TYPE -> R.string.browse_sort_order_file_type
|
||||
BrowseSortOrder.LAST_MODIFIED -> R.string.browse_sort_order_last_modified
|
||||
BrowseSortOrder.FILE_SIZE -> R.string.browse_sort_order_file_size
|
||||
}
|
||||
),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
@file:Suppress("FunctionName")
|
||||
|
||||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtLeast
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseFilterSetting
|
||||
|
||||
/**
|
||||
* Browse Filter subcategory.
|
||||
* Contains all settings from Filter Browse settings.
|
||||
*/
|
||||
fun LazyListScope.BrowseFilterSubcategory(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit,
|
||||
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
|
||||
title: @Composable () -> String = { stringResource(id = R.string.filter_browse_settings) },
|
||||
showTitle: Boolean = true,
|
||||
topPadding: Dp,
|
||||
bottomPadding: Dp
|
||||
) {
|
||||
item {
|
||||
if (showTitle) {
|
||||
CategoryTitle(
|
||||
title = title.invoke(),
|
||||
color = titleColor.invoke(),
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.padding(
|
||||
top = topPadding,
|
||||
bottom = 8.dp
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height((topPadding - 8.dp).coerceAtLeast(0.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BrowseFilterSetting(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent
|
||||
)
|
||||
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height(bottomPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
@file:Suppress("FunctionName")
|
||||
|
||||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtLeast
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseFilesStructureSetting
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseGridSizeSetting
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseLayoutSetting
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowsePinFavoriteDirectoriesSetting
|
||||
|
||||
/**
|
||||
* Browse General subcategory.
|
||||
* Contains all settings from General Browse settings.
|
||||
*/
|
||||
fun LazyListScope.BrowseGeneralSubcategory(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit,
|
||||
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
|
||||
title: @Composable () -> String = { stringResource(id = R.string.general_browse_settings) },
|
||||
showTitle: Boolean = true,
|
||||
topPadding: Dp,
|
||||
bottomPadding: Dp
|
||||
) {
|
||||
item {
|
||||
if (showTitle) {
|
||||
CategoryTitle(
|
||||
title = title.invoke(),
|
||||
color = titleColor.invoke(),
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.padding(
|
||||
top = topPadding,
|
||||
bottom = 8.dp
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height((topPadding - 8.dp).coerceAtLeast(0.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
BrowseFilesStructureSetting(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BrowsePinFavoriteDirectoriesSetting(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BrowseLayoutSetting(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BrowseGridSizeSetting(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height(bottomPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
@file:Suppress("FunctionName")
|
||||
|
||||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.subcategories
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtLeast
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.nested.browse.components.settings.BrowseSortOrderSetting
|
||||
|
||||
/**
|
||||
* Browse Sort subcategory.
|
||||
* Contains all settings from Sort Browse settings.
|
||||
*/
|
||||
fun LazyListScope.BrowseSortSubcategory(
|
||||
state: State<MainState>,
|
||||
onMainEvent: (MainEvent) -> Unit,
|
||||
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
|
||||
title: @Composable () -> String = { stringResource(id = R.string.sort_browse_settings) },
|
||||
showTitle: Boolean = true,
|
||||
topPadding: Dp,
|
||||
bottomPadding: Dp
|
||||
) {
|
||||
item {
|
||||
if (showTitle) {
|
||||
CategoryTitle(
|
||||
title = title.invoke(),
|
||||
color = titleColor.invoke(),
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.padding(
|
||||
top = topPadding,
|
||||
bottom = 8.dp
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height((topPadding - 8.dp).coerceAtLeast(0.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BrowseSortOrderSetting(
|
||||
state = state,
|
||||
onMainEvent = onMainEvent
|
||||
)
|
||||
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height(bottomPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.data
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
enum class BrowseFilesStructure {
|
||||
ALL_FILES, DIRECTORIES
|
||||
}
|
||||
|
||||
fun String.toFilesStructure(): BrowseFilesStructure {
|
||||
return BrowseFilesStructure.valueOf(this)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.data
|
||||
|
||||
enum class BrowseLayout {
|
||||
LIST, GRID
|
||||
}
|
||||
|
||||
fun String.toBrowseLayout(): BrowseLayout {
|
||||
return BrowseLayout.valueOf(this)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.nested.browse.data
|
||||
|
||||
enum class BrowseSortOrder {
|
||||
NAME,
|
||||
FILE_FORMAT,
|
||||
FILE_TYPE,
|
||||
LAST_MODIFIED,
|
||||
FILE_SIZE,
|
||||
}
|
||||
|
||||
fun String.toBrowseSortOrder(): BrowseSortOrder {
|
||||
return BrowseSortOrder.valueOf(this)
|
||||
}
|
||||
|
|
@ -187,6 +187,8 @@
|
|||
<string name="dropped_tab">Кинуто</string>
|
||||
<string name="general_tab">Головні</string>
|
||||
<string name="color_tab">Кольори</string>
|
||||
<string name="filter_tab">Фільтр</string>
|
||||
<string name="sort_tab">Сортування</string>
|
||||
|
||||
<!-- Settings categories -->
|
||||
<string name="general_settings">Головні</string>
|
||||
|
|
@ -195,6 +197,8 @@
|
|||
<string name="appearance_settings_desc">Тема, кольори</string>
|
||||
<string name="reader_settings">Читач</string>
|
||||
<string name="reader_settings_desc">Шрифт, текст, перекладач</string>
|
||||
<string name="browse_settings">Огляд</string>
|
||||
<string name="browse_settings_desc">Головні, фільтр, сортування</string>
|
||||
|
||||
<!-- Settings subcategories -->
|
||||
<string name="font_reader_settings">Шрифт</string>
|
||||
|
|
@ -202,6 +206,9 @@
|
|||
<string name="translator_reader_settings">Перекладач</string>
|
||||
<string name="theme_appearance_settings">Налаштування теми</string>
|
||||
<string name="colors_appearance_settings">Кольори</string>
|
||||
<string name="general_browse_settings">Головні</string>
|
||||
<string name="filter_browse_settings">Фільтр</string>
|
||||
<string name="sort_browse_settings">Сотрування</string>
|
||||
|
||||
<!-- Settings options -->
|
||||
<string name="language_option">Мова застосунка</string>
|
||||
|
|
@ -225,6 +232,13 @@
|
|||
<string name="double_click_translation_option_desc">Двічі клацніть на абзац, щоб перекласти його</string>
|
||||
<string name="fast_color_preset_change_option">Швидка зміна пресета кольорів</string>
|
||||
<string name="fast_color_preset_change_option_desc">Проведіть вліво або вправо, щоб змінити пресет кольорів</string>
|
||||
<string name="files_structure_option">Структура файлів</string>
|
||||
<string name="browse_layout_option">Режим відображення</string>
|
||||
<string name="browse_grid_size_option">Кількість комірок</string>
|
||||
<string name="browse_pin_favorite_directories_option">Закріпити улюблені директорії</string>
|
||||
<string name="browse_pin_favorite_directories_option_desc">
|
||||
Закріпіть ваші улюблені директорії зверху екрана
|
||||
</string>
|
||||
|
||||
<!-- Color presets -->
|
||||
<string name="red_color">Червоний</string>
|
||||
|
|
@ -251,6 +265,25 @@
|
|||
<string name="font_style_italic">Похилий</string>
|
||||
<string name="font_style_normal">Нормальний</string>
|
||||
|
||||
<!-- Files Structure properties -->
|
||||
<string name="files_structure_all">Всі файли</string>
|
||||
<string name="files_structure_directory">Директорії</string>
|
||||
|
||||
<!-- Browse Layout properties -->
|
||||
<string name="browse_layout_list">Список</string>
|
||||
<string name="browse_layout_grid">Сітка</string>
|
||||
|
||||
<!-- Browse Grid Size properties -->
|
||||
<string name="browse_grid_size_auto">Авто</string>
|
||||
<string name="browse_grid_size_per_row">кожен рядок</string>
|
||||
|
||||
<!-- Browse Sort Order properties -->
|
||||
<string name="browse_sort_order_name">За алфавітом</string>
|
||||
<string name="browse_sort_order_file_format">Формат файла</string>
|
||||
<string name="browse_sort_order_file_type">Тип файла</string>
|
||||
<string name="browse_sort_order_last_modified">Остання зміна</string>
|
||||
<string name="browse_sort_order_file_size">Розмір файла</string>
|
||||
|
||||
<!-- Themes -->
|
||||
<string name="dynamic_theme">Меркурій</string>
|
||||
<string name="blue_theme">Нептун</string>
|
||||
|
|
@ -293,6 +326,9 @@
|
|||
<string name="file_size">Розмір файлу</string>
|
||||
<string name="unknown">Невідомо</string>
|
||||
|
||||
<!-- Browse -->
|
||||
<string name="internal_storage">Внутрішнє Сховище</string>
|
||||
|
||||
<!-- Reader -->
|
||||
<string name="loading">Завантаження…</string>
|
||||
<string name="happy_reading">Приємного читання!</string>
|
||||
|
|
@ -512,6 +548,10 @@
|
|||
<string name="open_reader_settings_content_desc">Налаштування читача</string>
|
||||
<string name="top_app_bar_anim_content_desc">Анімація верхньої панелі</string>
|
||||
<string name="file_icon_content_desc">Файл</string>
|
||||
<string name="directory_icon_content_desc">Тека</string>
|
||||
<string name="favorite_directory_content_desc">Улюблена тека</string>
|
||||
<string name="sort_order_content_desc">Порядок сортування</string>
|
||||
<string name="path_arrow_icon_content_desc">Стрілка шляху</string>
|
||||
<string name="checkbox_content_desc">Галочка</string>
|
||||
<string name="show_dropdown_content_desc">Випадаючий список</string>
|
||||
<string name="cover_image_not_found_content_desc">Обкладенка не знайдена</string>
|
||||
|
|
@ -520,6 +560,7 @@
|
|||
<string name="search_content_desc">Пошук</string>
|
||||
<string name="clear_selected_items_content_desc">Очистити обрані предмети</string>
|
||||
<string name="add_files_content_desc">Додати файли</string>
|
||||
<string name="select_all_files_content_desc">Вибрати всі файли</string>
|
||||
<string name="exit_search_content_desc">Вийти з пошуку</string>
|
||||
<string name="delete_whole_history_content_desc">Видалити всю історію</string>
|
||||
<string name="move_books_content_desc">Перемістити обрані книги</string>
|
||||
|
|
@ -545,5 +586,6 @@
|
|||
<string name="github_project_content_desc">GitHub Проєкт</string>
|
||||
<string name="github_profile_content_desc">GitHub Профіль</string>
|
||||
<string name="drag_content_desc">Тягнути</string>
|
||||
<string name="filter_content_desc">Фільтр</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -206,6 +206,8 @@
|
|||
<string name="dropped_tab">Dropped</string>
|
||||
<string name="general_tab">General</string>
|
||||
<string name="color_tab">Colors</string>
|
||||
<string name="filter_tab">Filter</string>
|
||||
<string name="sort_tab">Sort</string>
|
||||
|
||||
<!-- Settings categories -->
|
||||
<string name="general_settings">General</string>
|
||||
|
|
@ -214,6 +216,8 @@
|
|||
<string name="appearance_settings_desc">Theme, colors</string>
|
||||
<string name="reader_settings">Reader</string>
|
||||
<string name="reader_settings_desc">Font, text, translator</string>
|
||||
<string name="browse_settings">Browse</string>
|
||||
<string name="browse_settings_desc">General, filter, sort</string>
|
||||
|
||||
<!-- Settings subcategories -->
|
||||
<string name="font_reader_settings">Font</string>
|
||||
|
|
@ -221,6 +225,9 @@
|
|||
<string name="translator_reader_settings">Translator</string>
|
||||
<string name="theme_appearance_settings">Theme preferences</string>
|
||||
<string name="colors_appearance_settings">Colors</string>
|
||||
<string name="general_browse_settings">General</string>
|
||||
<string name="filter_browse_settings">Filter</string>
|
||||
<string name="sort_browse_settings">Sort</string>
|
||||
|
||||
<!-- Settings options -->
|
||||
<string name="language_option">App language</string>
|
||||
|
|
@ -244,6 +251,13 @@
|
|||
<string name="double_click_translation_option_desc">Double click on paragraph to translate it</string>
|
||||
<string name="fast_color_preset_change_option">Fast color preset change</string>
|
||||
<string name="fast_color_preset_change_option_desc">Swipe left or right to change color preset</string>
|
||||
<string name="files_structure_option">Files structure</string>
|
||||
<string name="browse_layout_option">Display mode</string>
|
||||
<string name="browse_grid_size_option">Grid size</string>
|
||||
<string name="browse_pin_favorite_directories_option">Pin favorite directories</string>
|
||||
<string name="browse_pin_favorite_directories_option_desc">
|
||||
Pin your favorite directories to the top of the screen
|
||||
</string>
|
||||
|
||||
<!-- Color presets -->
|
||||
<string name="red_color">Red</string>
|
||||
|
|
@ -270,6 +284,25 @@
|
|||
<string name="font_style_italic">Italic</string>
|
||||
<string name="font_style_normal">Normal</string>
|
||||
|
||||
<!-- Files Structure properties -->
|
||||
<string name="files_structure_all">All files</string>
|
||||
<string name="files_structure_directory">Directories</string>
|
||||
|
||||
<!-- Browse Layout properties -->
|
||||
<string name="browse_layout_list">List</string>
|
||||
<string name="browse_layout_grid">Grid</string>
|
||||
|
||||
<!-- Browse Grid Size properties -->
|
||||
<string name="browse_grid_size_auto">Auto</string>
|
||||
<string name="browse_grid_size_per_row">per row</string>
|
||||
|
||||
<!-- Browse Sort Order properties -->
|
||||
<string name="browse_sort_order_name">Alphabetically</string>
|
||||
<string name="browse_sort_order_file_format">File format</string>
|
||||
<string name="browse_sort_order_file_type">File type</string>
|
||||
<string name="browse_sort_order_last_modified">Last modified</string>
|
||||
<string name="browse_sort_order_file_size">File size</string>
|
||||
|
||||
<!-- Themes -->
|
||||
<string name="dynamic_theme">Mercury</string>
|
||||
<string name="blue_theme">Neptune</string>
|
||||
|
|
@ -312,6 +345,9 @@
|
|||
<string name="file_size">File size</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
|
||||
<!-- Browse -->
|
||||
<string name="internal_storage">Internal Storage</string>
|
||||
|
||||
<!-- Reader -->
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="happy_reading">Happy reading!</string>
|
||||
|
|
@ -530,6 +566,10 @@
|
|||
<string name="open_reader_settings_content_desc">Reader settings</string>
|
||||
<string name="top_app_bar_anim_content_desc">Top app bar animation</string>
|
||||
<string name="file_icon_content_desc">File</string>
|
||||
<string name="directory_icon_content_desc">Directory</string>
|
||||
<string name="favorite_directory_content_desc">Favorite directory</string>
|
||||
<string name="sort_order_content_desc">Sort order</string>
|
||||
<string name="path_arrow_icon_content_desc">Path arrow</string>
|
||||
<string name="checkbox_content_desc">Checkbox</string>
|
||||
<string name="show_dropdown_content_desc">Dropdown</string>
|
||||
<string name="cover_image_not_found_content_desc">Cover image not found</string>
|
||||
|
|
@ -538,6 +578,7 @@
|
|||
<string name="search_content_desc">Search</string>
|
||||
<string name="clear_selected_items_content_desc">Clear selected items</string>
|
||||
<string name="add_files_content_desc">Add files</string>
|
||||
<string name="select_all_files_content_desc">Select all files</string>
|
||||
<string name="exit_search_content_desc">Exit search</string>
|
||||
<string name="delete_whole_history_content_desc">Delete whole history</string>
|
||||
<string name="move_books_content_desc">Move selected books</string>
|
||||
|
|
@ -565,5 +606,6 @@
|
|||
<string name="github_project_content_desc">GitHub Project</string>
|
||||
<string name="github_profile_content_desc">GitHub Profile</string>
|
||||
<string name="drag_content_desc">Drag</string>
|
||||
<string name="filter_content_desc">Filter</string>
|
||||
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue