From cee3d00bb17f4f6f3998d91ac5b0a9af6c9cabf0 Mon Sep 17 00:00:00 2001 From: Robin Meersman Date: Mon, 11 Nov 2024 18:10:30 +0000 Subject: [PATCH] Frontend - directory provider --- .../main/java/be/re/writand/MainActivity.kt | 4 - .../local/filemanager/FileManagerLocal.kt | 10 + .../data/local/filemanager/IFileManager.kt | 9 + .../domain/files/ListDirectoriesUseCase.kt | 23 ++ .../re/writand/screens/components/PopUps.kt | 205 +++++++++++ .../re/writand/screens/components/WPopup.kt | 7 + .../DirectoryProviderScreen.kt | 323 ++++++++++++++++++ .../DirectoryProviderUiState.kt | 27 ++ .../DirectoryProviderViewModel.kt | 94 +++++ .../screens/filetree/WTreeComponent.kt | 153 +-------- .../utils/directorypicker/DirectoryPicker.kt | 44 --- .../IDirectoryPickerListener.kt | 7 - .../baseline_create_new_folder_16.xml | 5 + .../res/drawable/baseline_download_16.xml | 5 + .../main/res/drawable/baseline_home_16.xml | 5 + .../main/res/drawable/baseline_image_16.xml | 5 + .../baseline_insert_drive_file_16.xml | 5 + .../drawable/baseline_library_music_16.xml | 5 + .../res/drawable/baseline_video_file_16.xml | 5 + app/src/main/res/drawable/round_folder_16.xml | 5 + 20 files changed, 743 insertions(+), 203 deletions(-) create mode 100644 app/src/main/java/be/re/writand/domain/files/ListDirectoriesUseCase.kt create mode 100644 app/src/main/java/be/re/writand/screens/components/PopUps.kt create mode 100644 app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderScreen.kt create mode 100644 app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderUiState.kt create mode 100644 app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderViewModel.kt delete mode 100644 app/src/main/java/be/re/writand/utils/directorypicker/DirectoryPicker.kt delete mode 100644 app/src/main/java/be/re/writand/utils/directorypicker/IDirectoryPickerListener.kt create mode 100644 app/src/main/res/drawable/baseline_create_new_folder_16.xml create mode 100644 app/src/main/res/drawable/baseline_download_16.xml create mode 100644 app/src/main/res/drawable/baseline_home_16.xml create mode 100644 app/src/main/res/drawable/baseline_image_16.xml create mode 100644 app/src/main/res/drawable/baseline_insert_drive_file_16.xml create mode 100644 app/src/main/res/drawable/baseline_library_music_16.xml create mode 100644 app/src/main/res/drawable/baseline_video_file_16.xml create mode 100644 app/src/main/res/drawable/round_folder_16.xml diff --git a/app/src/main/java/be/re/writand/MainActivity.kt b/app/src/main/java/be/re/writand/MainActivity.kt index 69db411..b9f2e8e 100644 --- a/app/src/main/java/be/re/writand/MainActivity.kt +++ b/app/src/main/java/be/re/writand/MainActivity.kt @@ -13,7 +13,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -21,14 +20,11 @@ import androidx.compose.ui.Modifier import androidx.core.app.ActivityCompat import androidx.navigation.compose.rememberNavController import be.re.writand.navigation.WNavGraph -import be.re.writand.screens.filetree.WFiletree import be.re.writand.ui.theme.WritandTheme -import be.re.writand.utils.directorypicker.DirectoryPicker import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { - private val picker = DirectoryPicker(this) private val EXTERNAL_STORAGE_PERMISSION_CODE = 100 private val EXTERNAL_STORAGE_PERMISSION = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION private var hasPermission = false diff --git a/app/src/main/java/be/re/writand/data/local/filemanager/FileManagerLocal.kt b/app/src/main/java/be/re/writand/data/local/filemanager/FileManagerLocal.kt index f7e9547..3a60c69 100644 --- a/app/src/main/java/be/re/writand/data/local/filemanager/FileManagerLocal.kt +++ b/app/src/main/java/be/re/writand/data/local/filemanager/FileManagerLocal.kt @@ -5,6 +5,9 @@ import be.re.writand.utils.GenerateId import be.re.writand.utils.WFileTreeAbstraction import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import okio.Path.Companion.toPath +import java.io.File +import java.io.FileFilter import java.nio.file.Files import java.nio.file.NotDirectoryException import java.nio.file.Path @@ -138,4 +141,11 @@ class FileManagerLocal @Inject constructor( val file = path.toFile() return file.readLines().joinToString(separator = "\n") } + + override suspend fun listFiles(path: Path, filter: FileFilter?): Array { + val file = path.toFile() + val dirs = file.listFiles(filter) + + return dirs ?: arrayOf() + } } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/data/local/filemanager/IFileManager.kt b/app/src/main/java/be/re/writand/data/local/filemanager/IFileManager.kt index 9b89dc6..64c7293 100644 --- a/app/src/main/java/be/re/writand/data/local/filemanager/IFileManager.kt +++ b/app/src/main/java/be/re/writand/data/local/filemanager/IFileManager.kt @@ -1,6 +1,8 @@ package be.re.writand.data.local.filemanager import be.re.writand.utils.WFileTreeAbstraction +import java.io.File +import java.io.FileFilter import java.io.IOException import java.nio.file.InvalidPathException import java.nio.file.NotDirectoryException @@ -65,4 +67,11 @@ interface IFileManager { * @throws IOException */ suspend fun read(path: Path): String + + /** + * List all the files in a given directory. + * @param[path] the path to list the files in. + * @param[filter] an optional filter used to filter the items. + */ + suspend fun listFiles(path: Path, filter: FileFilter?): Array } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/domain/files/ListDirectoriesUseCase.kt b/app/src/main/java/be/re/writand/domain/files/ListDirectoriesUseCase.kt new file mode 100644 index 0000000..f3e865a --- /dev/null +++ b/app/src/main/java/be/re/writand/domain/files/ListDirectoriesUseCase.kt @@ -0,0 +1,23 @@ +package be.re.writand.domain.files + +import be.re.writand.data.local.filemanager.IFileManager +import java.io.File +import java.io.FileFilter +import java.nio.file.Paths +import javax.inject.Inject + +/** + * Use case to list all the directories for a given path. + * @param[manager] an object implementing the [IFileManager] interface. + */ +class ListDirectoriesUseCase @Inject constructor( + private val manager: IFileManager +) { + private val filter = FileFilter { + it.isDirectory + } + + suspend operator fun invoke(path: String): Array { + return manager.listFiles(Paths.get(path), filter) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/components/PopUps.kt b/app/src/main/java/be/re/writand/screens/components/PopUps.kt new file mode 100644 index 0000000..bc2de0c --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/components/PopUps.kt @@ -0,0 +1,205 @@ +package be.re.writand.screens.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import be.re.writand.data.local.filemanager.FileType +import be.re.writand.screens.WUIGlobals +import be.re.writand.ui.theme.MainGreen + +/** + * Enum for the create file pop up to switch between different options. + * Controls what the create file pop up stands for: creating files, directories or both. + */ +enum class CreationType { + FILE, + DIRECTORY, + ALL +} + +/** + * The top bar used in the pop ups. + * TODO: change with component after merging with main. + * @see[RemoveFilePopUp] + * @see[CreateFile] for the actual pop ups. + * @param[title] the title of the pop up. + * @param[showExit] a boolean (default to true) whether or not the exit button needs to be shown. + * @param[onDismiss] a function belonging to the exit button if it needs to be shown (default is the empty function). + */ +@Composable +private fun TopPopUpBar(title: String, showExit: Boolean = true, onDismiss: () -> Unit = {}) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(45.dp) + .background(MainGreen) + .border( + width = 1.dp, + color = MainGreen, + shape = RoundedCornerShape( + topStart = WUIGlobals.cornerRadius, topEnd = WUIGlobals.cornerRadius, + bottomStart = 0.dp, bottomEnd = 0.dp + ) + ) + .padding(5.dp) + ) { + Spacer(modifier = Modifier.width(0.dp)) + WText(text = title) + if (showExit) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "", + modifier = Modifier.clickable { + onDismiss() + }) + } else { + Spacer(modifier = Modifier.width(0.dp)) + } + } +} + +/** + * Pop up screen to create a file / directory. + * @param[type] a value telling the popup what should be created. Values can be FILE, DIRECTORY or + * ALL to allow both files and directories (user can switch by clicking a checkbox. + * @param[hidePopUp] a function telling this component what to do when closing this pop up is requested. + * @param[onConfirm] a function performing the action when the user confirms the creation of a new file / directory. + */ +@Composable +fun CreateFilePopUp( + type: CreationType, + hidePopUp: () -> Unit, + onConfirm: (String, FileType) -> Unit +) { + val (fileName, setFileName) = remember { mutableStateOf("") } + val title = when (type) { + CreationType.FILE -> "Create a new file" + CreationType.DIRECTORY -> "Create a new directory" + CreationType.ALL -> "Create a new file / directory" + } + WPopup( + titleBar = { TopPopUpBar(title = title, onDismiss = hidePopUp) }, + bottomBar = { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + ) { + val (isDirectory, setIsDirectory) = remember { mutableStateOf(false) } + + if (type == CreationType.ALL) { + WCheckbox(text = "make directory", checked = isDirectory) { + setIsDirectory(!isDirectory) + } + } else { + Spacer(modifier = Modifier.width(0.dp)) // just to split this row into 2 + } + + + WButton(text = "Create", onClick = { + val fileType = if (isDirectory) FileType.DIRECTORY else FileType.FILE + hidePopUp() // TODO: show error if field is empty + onConfirm(fileName, fileType) + }) + } + }, + width = 500.dp, + height = 200.dp, + onDismiss = hidePopUp + ) { padding -> + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) { + Row( + modifier = Modifier.padding(10.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + WLabelAndTextField( + title = "Filename", + value = fileName, + onTextChange = { setFileName(it) } + ) + } + } + } +} + +/** + * The pop up asking for confirmation when about to delete a file / directory. + * @param[filename] the name of the file / directory which is about to be deleted. + * @param[hidePopUp] a function implementing the logic to hide this pop up. + * @param[onConfirm] a function implementing the logic to actually delete this file / directory. + */ +@Composable +fun RemoveFilePopUp(filename: String, hidePopUp: () -> Unit, onConfirm: () -> Unit) { + WPopup( + titleBar = { + TopPopUpBar( + title = "Are you sure?", + showExit = false + ) + }, + bottomBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + WBorderButton( + text = "Cancel", + onClick = hidePopUp + ) + + WDangerButton( + text = "Delete", + onClick = { + onConfirm() + hidePopUp() + } + ) + } + }, + height = 200.dp, + width = 500.dp, + onDismiss = hidePopUp + ) { padding -> + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) { + WText("Do you really want to delete $filename?") + } + } +} diff --git a/app/src/main/java/be/re/writand/screens/components/WPopup.kt b/app/src/main/java/be/re/writand/screens/components/WPopup.kt index 025ff77..18292ce 100644 --- a/app/src/main/java/be/re/writand/screens/components/WPopup.kt +++ b/app/src/main/java/be/re/writand/screens/components/WPopup.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.FabPosition import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.AbsoluteAlignment @@ -31,6 +32,8 @@ import be.re.writand.ui.theme.MainGreen * @param[onDismiss] a function to be executed when the pop up must hide. * @param[modifier] the modifier for further styling, applied on the outer box of this component. * @param[children] a composable with the content inside the pop up. + * @param[floatingButton] a composable to represent the floating action button for the Scaffold. + * @param[floatingButtonPosition] the position for the floating button composable. */ @Composable fun WPopup( @@ -40,6 +43,8 @@ fun WPopup( onDismiss: () -> Unit, modifier: Modifier = Modifier, bottomBar: (@Composable () -> Unit) = {}, + floatingButton: @Composable () -> Unit = {}, + floatingButtonPosition: FabPosition = FabPosition.End, children: @Composable (PaddingValues) -> Unit = {} ) { Dialog( @@ -56,6 +61,8 @@ fun WPopup( topBar = titleBar, bottomBar = bottomBar, content = children, + floatingActionButton = floatingButton, + floatingActionButtonPosition = floatingButtonPosition, modifier = Modifier .fillMaxSize() .border( diff --git a/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderScreen.kt b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderScreen.kt new file mode 100644 index 0000000..8508567 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderScreen.kt @@ -0,0 +1,323 @@ +package be.re.writand.screens.directoryprovider + +import android.os.Environment +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import be.re.writand.R +import be.re.writand.screens.WUIGlobals +import be.re.writand.screens.components.CreateFilePopUp +import be.re.writand.screens.components.CreationType +import be.re.writand.screens.components.WButton +import be.re.writand.screens.components.WLoadingIndicator +import be.re.writand.screens.components.WPopup +import be.re.writand.screens.components.WText +import be.re.writand.ui.theme.MainGreen + +@Composable +private fun TopBar(onDismiss: () -> Unit, onBack: () -> Unit) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .background(MainGreen) + .border( + width = 1.dp, + color = MainGreen, + shape = RoundedCornerShape( + topStart = WUIGlobals.cornerRadius, topEnd = WUIGlobals.cornerRadius, + bottomStart = 0.dp, bottomEnd = 0.dp + ) + ) + .padding(10.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Go up a directory", + modifier = Modifier + .size(WUIGlobals.iconSize) + .clickable(onClick = onBack) + ) + WText(text = "Choose a directory") + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close this pop up", + modifier = Modifier + .size(WUIGlobals.iconSize) + .clickable(onClick = onDismiss) + ) + } +} + +@Composable +private fun DirectoryIcon( + name: String, + isSelected: Boolean, + onSelect: () -> Unit, + onDoubleClick: () -> Unit +) { + var modifier = Modifier.size(width = 80.dp, height = 130.dp) + if (isSelected) { + modifier = modifier + .clip(RoundedCornerShape(WUIGlobals.cornerRadius)) + .background(Color.White) + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier + .pointerInput(Unit) { + detectTapGestures( + onTap = { onSelect() }, + onDoubleTap = { onDoubleClick() } + ) + } + ) { + Icon( + painter = painterResource(R.drawable.round_folder_16), + contentDescription = "Folder icon", + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier + .size(60.dp) + ) + WText(text = name) + } +} + +@Composable +private fun FloatingButton(onClick: () -> Unit) { + FloatingActionButton( + onClick = onClick, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.onSecondary, + modifier = Modifier + .size(60.dp) + .offset(x = 0.dp, y = (-70).dp) + ) { + Icon( + painter = painterResource(R.drawable.baseline_create_new_folder_16), + contentDescription = "Create a new folder", + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier.size(40.dp) + ) + } +} + +@Composable +private fun ShortcutColumnItem(name: String, icon: Int, onClick: () -> Unit) { + Row( + horizontalArrangement = Arrangement.spacedBy(space = 10.dp, alignment = Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(10.dp) + .clickable(onClick = onClick) + ) { + Icon( + painter = painterResource(icon), + contentDescription = "Shortcut icon", + modifier = Modifier.size(WUIGlobals.iconSize), + tint = MaterialTheme.colorScheme.tertiary + ) + WText(text = name, fontWeight = FontWeight.SemiBold) + } +} + +@Composable +fun DirectoryProvider( + vm: DirectoryProviderViewModel = hiltViewModel(), + onDismiss: () -> Unit, + onConfirm: (String) -> Unit +) { + // The shortcuts used in the sidebar, each element is a pair that links the name to its path + val shortcuts = mapOf( + Pair("Documents", Environment.DIRECTORY_DOCUMENTS), + Pair("Downloads", Environment.DIRECTORY_DOWNLOADS), + Pair("Music", Environment.DIRECTORY_MUSIC), + Pair("Pictures", Environment.DIRECTORY_PICTURES), + Pair("Videos", Environment.DIRECTORY_MOVIES) + ) + + val icons = listOf( + R.drawable.baseline_insert_drive_file_16, + R.drawable.baseline_download_16, + R.drawable.baseline_library_music_16, + R.drawable.baseline_image_16, + R.drawable.baseline_video_file_16 + ) + + val currentPath = vm.currentPath.joinToString(vm.separator) + val uiState by vm.uiState.collectAsState() + val selectedItem by vm.selectedItem + val (showCreateDirectory, setShowCreateDirectory) = remember { mutableStateOf(false) } + + WPopup( + width = 1000.dp, + height = 800.dp, + onDismiss = onDismiss, + titleBar = { TopBar(onDismiss = onDismiss, onBack = { vm.moveUp() }) }, + floatingButton = { + FloatingButton(onClick = { + setShowCreateDirectory(true) + }) + }, + bottomBar = { + HorizontalDivider(color = MainGreen) + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + ) { + val path = currentPath.ifEmpty { + "" + } + + WText("Current path: $path", maxLines = 1, minLines = 1) + } + } + ) { padding -> + // Pop ups + if (showCreateDirectory) CreateFilePopUp( + type = CreationType.DIRECTORY, + hidePopUp = { setShowCreateDirectory(false) }, + onConfirm = { name, _ -> vm.createNewDirectory(name) } + ) + + // Main UI element + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Row { + // Sidebar + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth(fraction = 1f / 5) + .fillMaxHeight() + .background(MaterialTheme.colorScheme.primary) + .padding(10.dp) + ) { + ShortcutColumnItem(name = "Home", icon = R.drawable.baseline_home_16) { + vm.resetPath() + } + shortcuts.entries.zip(icons).forEach { (entry, icon) -> + ShortcutColumnItem(name = entry.key, icon = icon) { + vm.setShortcut(entry.value) + } + } + } + + VerticalDivider(color = MainGreen) + + // Directory contents + when (val s = uiState) { + DirectoryProviderUiState.Empty -> { + WText("No path is opened yet...") + } + + DirectoryProviderUiState.Loading -> { + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + WLoadingIndicator() + } + } + + is DirectoryProviderUiState.Failed -> { + WText(s.message) + } + + is DirectoryProviderUiState.Success -> { + Column(modifier = Modifier.background(MaterialTheme.colorScheme.secondary)) { + LazyVerticalGrid( + columns = GridCells.Fixed(5), + contentPadding = PaddingValues(10.dp), + horizontalArrangement = Arrangement.spacedBy( + 10.dp, + Alignment.Start + ), + verticalArrangement = Arrangement.spacedBy( + 10.dp, + Alignment.Top + ), + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(fraction = 0.90f) + .background(MaterialTheme.colorScheme.secondary) + ) { + items(s.files, key = { file -> file.name }) { f -> + DirectoryIcon( + f.name, + isSelected = f.name == selectedItem, + onSelect = { vm.replaceLast(f.name) }, + onDoubleClick = { vm.enterDirectory(f.name) } + ) + } + } + // Row for confirmation button + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(10.dp) + .background(MaterialTheme.colorScheme.secondary) + ) { + WButton(text = "Select", onClick = { onConfirm(currentPath) }) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderUiState.kt b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderUiState.kt new file mode 100644 index 0000000..3d75143 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderUiState.kt @@ -0,0 +1,27 @@ +package be.re.writand.screens.directoryprovider + +import java.io.File + +sealed interface DirectoryProviderUiState { + + data object Empty : DirectoryProviderUiState + data object Loading : DirectoryProviderUiState + data class Success(val files: Array) : DirectoryProviderUiState { + + // Required to have custom implementations for these because of the Array parameter + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Success + + return files.contentEquals(other.files) + } + + override fun hashCode(): Int { + return files.contentHashCode() + } + } + + data class Failed(val message: String) : DirectoryProviderUiState +} diff --git a/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderViewModel.kt b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderViewModel.kt new file mode 100644 index 0000000..281ddbd --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/directoryprovider/DirectoryProviderViewModel.kt @@ -0,0 +1,94 @@ +package be.re.writand.screens.directoryprovider + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import be.re.writand.data.local.filemanager.FileType +import be.re.writand.domain.files.CreateFileUseCase +import be.re.writand.domain.files.ListDirectoriesUseCase +import be.re.writand.screens.WViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import java.io.File +import java.nio.file.Paths +import javax.inject.Inject + +/** + * Viewmodel for the directory provider pop up. + * Holds the currently selected path in a state, and gives this back as a result. + * @param[listDirectoriesUseCase] use case to list only the directories in a given path. + * @param[createFileUseCase] use case to make a new file (in this case: directory). + */ +@HiltViewModel +class DirectoryProviderViewModel @Inject constructor( + private val listDirectoriesUseCase: ListDirectoriesUseCase, + private val createFileUseCase: CreateFileUseCase +) : WViewModel() { + private val root = "/storage/emulated/0" + val separator: String = File.separator + + private val _uiState = + MutableStateFlow(DirectoryProviderUiState.Empty) + val uiState: StateFlow = _uiState + + val currentPath = mutableStateListOf(root) + val selectedItem = mutableStateOf(null) + + init { + listCurrentDirectory() + } + + fun createNewDirectory(name: String) { + launchCatching { + val path = Paths.get(currentPath.joinToString(separator)) + createFileUseCase(name, FileType.DIRECTORY, path) + } + listCurrentDirectory() + } + + private fun listCurrentDirectory() { + launchCatching { + val files = listDirectoriesUseCase(currentPath.joinToString(separator)) + _uiState.value = DirectoryProviderUiState.Success(files) + } + } + + fun replaceLast(name: String) { + if (currentPath.size > 1 && selectedItem.value != null) { + currentPath[currentPath.lastIndex] = name + } else { + currentPath.add(name) + } + selectedItem.value = name + } + + fun enterDirectory(name: String) { + if (selectedItem.value == null) { + currentPath.add(name) + } else if (currentPath.last() != name) { + replaceLast(name) + } + listCurrentDirectory() + } + + fun moveUp() { + if (currentPath.size > 1) { + currentPath.removeLast() + selectedItem.value = null + listCurrentDirectory() + } + } + + fun setShortcut(shortcut: String) { + currentPath.removeRange(1, currentPath.size) + currentPath.add(shortcut) + selectedItem.value = null + listCurrentDirectory() + } + + fun resetPath() { + currentPath.removeRange(1, currentPath.size) + selectedItem.value = null + listCurrentDirectory() + } +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt b/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt index 35dd367..790cb2f 100644 --- a/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt +++ b/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt @@ -35,6 +35,9 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import be.re.writand.data.local.filemanager.FileType import be.re.writand.screens.WUIGlobals +import be.re.writand.screens.components.CreateFilePopUp +import be.re.writand.screens.components.CreationType +import be.re.writand.screens.components.RemoveFilePopUp import be.re.writand.screens.components.WBorderButton import be.re.writand.screens.components.WButton import be.re.writand.screens.components.WCheckbox @@ -188,7 +191,8 @@ fun LazyListScope.WTreeItem( ) } - if (showAddPopUp) CreateFile( + if (showAddPopUp) CreateFilePopUp( + type = CreationType.ALL, hidePopUp = { setShowAddPopUp(false) }, onConfirm = { name, type -> onCreate(root, name, type) } ) @@ -263,150 +267,3 @@ fun Filename( } } } - -/** - * Pop up screen to create a file / directory. - * @param[hidePopUp] a function telling this component what to do when closing this pop up is requested. - * @param[onConfirm] a function performing the action when the user confirms the creation of a new file / directory. - */ -@Composable -fun CreateFile(hidePopUp: () -> Unit, onConfirm: (String, FileType) -> Unit) { - val (fileName, setFileName) = remember { mutableStateOf("") } - WPopup( - titleBar = { TopPopUpBar(title = "Create a new file / directory", onDismiss = hidePopUp) }, - bottomBar = { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(10.dp) - ) { - val (isDirectory, setIsDirectory) = remember { mutableStateOf(false) } - - WCheckbox(text = "make directory", checked = isDirectory) { - setIsDirectory(!isDirectory) - } - - WButton(text = "Create", onClick = { - val type = if (isDirectory) FileType.DIRECTORY else FileType.FILE - hidePopUp() // TODO: show error if field is empty - onConfirm(fileName, type) - }) - } - }, - width = 500.dp, - height = 200.dp, - onDismiss = hidePopUp - ) { padding -> - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier - .padding(padding) - .fillMaxSize() - ) { - Row( - modifier = Modifier.padding(10.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - WLabelAndTextField( - title = "Filename", - value = fileName, - onTextChange = { setFileName(it) } - ) - } - } - } -} - -/** - * The pop up asking for confirmation when about to delete a file / directory. - * @param[filename] the name of the file / directory which is about to be deleted. - * @param[hidePopUp] a function implementing the logic to hide this pop up. - * @param[onConfirm] a function implementing the logic to actually delete this file / directory. - */ -@Composable -fun RemoveFilePopUp(filename: String, hidePopUp: () -> Unit, onConfirm: () -> Unit) { - WPopup( - titleBar = { - TopPopUpBar( - title = "Are you sure?", - showExit = false - ) - }, - bottomBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(10.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - WBorderButton( - text = "Cancel", - onClick = hidePopUp - ) - - WDangerButton( - text = "Delete", - onClick = { - onConfirm() - hidePopUp() - } - ) - } - }, - height = 200.dp, - width = 500.dp, - onDismiss = hidePopUp - ) { padding -> - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .padding(padding) - .fillMaxSize() - ) { - WText("Do you really want to delete $filename?") - } - } -} - -/** - * The top bar used in the pop ups. - * @see[RemoveFilePopUp] - * @see[CreateFile] for the actual pop ups. - * @param[title] the title of the pop up. - * @param[showExit] a boolean (default to true) whether or not the exit button needs to be shown. - * @param[onDismiss] a function belonging to the exit button if it needs to be shown (default is the empty function). - */ -@Composable -private fun TopPopUpBar(title: String, showExit: Boolean = true, onDismiss: () -> Unit = {}) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(45.dp) - .background(MainGreen) - .border( - width = 1.dp, - color = MainGreen, - shape = RoundedCornerShape( - topStart = WUIGlobals.cornerRadius, topEnd = WUIGlobals.cornerRadius, - bottomStart = 0.dp, bottomEnd = 0.dp - ) - ) - .padding(5.dp) - ) { - Spacer(modifier = Modifier.width(0.dp)) - WText(text = title) - if (showExit) { - Icon(imageVector = Icons.Default.Close, contentDescription = "", modifier = Modifier.clickable { - onDismiss() - }) - } else { - Spacer(modifier = Modifier.width(0.dp)) - } - } -} diff --git a/app/src/main/java/be/re/writand/utils/directorypicker/DirectoryPicker.kt b/app/src/main/java/be/re/writand/utils/directorypicker/DirectoryPicker.kt deleted file mode 100644 index 9137b77..0000000 --- a/app/src/main/java/be/re/writand/utils/directorypicker/DirectoryPicker.kt +++ /dev/null @@ -1,44 +0,0 @@ -package be.re.writand.utils.directorypicker - -import android.content.Intent -import android.net.Uri -import android.util.Log -import androidx.activity.ComponentActivity -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts - -// TODO: make this class a builder-pattern -class DirectoryPicker(private var activity: ComponentActivity) { - private lateinit var listener: IDirectoryPickerListener - private var launcher = registerActivityResult() - - private fun registerActivityResult(): ActivityResultLauncher { - return activity.registerForActivityResult( - contract = ActivityResultContracts.OpenDocumentTree(), - callback = { handleResult(it) } - ) - } - - fun setActivity(activity: ComponentActivity) { - this.activity = activity - launcher = registerActivityResult() - } - - fun setListener(listener: IDirectoryPickerListener) { - this.listener = listener - } - - fun openPicker() { - launcher.launch(null) - } - - private fun handleResult(data: Uri?) { - Log.d("Filetree", "data = ${data.toString()}") - data?.let { - val flags = - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - activity.contentResolver.takePersistableUriPermission(data, flags) - if(this::listener.isInitialized) listener.notify(data) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/utils/directorypicker/IDirectoryPickerListener.kt b/app/src/main/java/be/re/writand/utils/directorypicker/IDirectoryPickerListener.kt deleted file mode 100644 index 325c6b4..0000000 --- a/app/src/main/java/be/re/writand/utils/directorypicker/IDirectoryPickerListener.kt +++ /dev/null @@ -1,7 +0,0 @@ -package be.re.writand.utils.directorypicker - -import android.net.Uri - -interface IDirectoryPickerListener { - fun notify(uri: Uri) -} \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_create_new_folder_16.xml b/app/src/main/res/drawable/baseline_create_new_folder_16.xml new file mode 100644 index 0000000..b16f542 --- /dev/null +++ b/app/src/main/res/drawable/baseline_create_new_folder_16.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_download_16.xml b/app/src/main/res/drawable/baseline_download_16.xml new file mode 100644 index 0000000..dfb59a8 --- /dev/null +++ b/app/src/main/res/drawable/baseline_download_16.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_home_16.xml b/app/src/main/res/drawable/baseline_home_16.xml new file mode 100644 index 0000000..6b6fd9b --- /dev/null +++ b/app/src/main/res/drawable/baseline_home_16.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_image_16.xml b/app/src/main/res/drawable/baseline_image_16.xml new file mode 100644 index 0000000..e7e4fbf --- /dev/null +++ b/app/src/main/res/drawable/baseline_image_16.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_insert_drive_file_16.xml b/app/src/main/res/drawable/baseline_insert_drive_file_16.xml new file mode 100644 index 0000000..1b0e146 --- /dev/null +++ b/app/src/main/res/drawable/baseline_insert_drive_file_16.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_library_music_16.xml b/app/src/main/res/drawable/baseline_library_music_16.xml new file mode 100644 index 0000000..3dbd545 --- /dev/null +++ b/app/src/main/res/drawable/baseline_library_music_16.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_video_file_16.xml b/app/src/main/res/drawable/baseline_video_file_16.xml new file mode 100644 index 0000000..9cce2f0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_video_file_16.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_folder_16.xml b/app/src/main/res/drawable/round_folder_16.xml new file mode 100644 index 0000000..cd47860 --- /dev/null +++ b/app/src/main/res/drawable/round_folder_16.xml @@ -0,0 +1,5 @@ + + + + +