forked from Writand/writand
Merge branch 'frontend/directoryprovider' into 'main'
Frontend - directory provider Closes #28 See merge request EmmaVandewalle/writand!46
This commit is contained in:
commit
61200e0502
20 changed files with 743 additions and 203 deletions
|
@ -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
|
||||
|
|
|
@ -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<File> {
|
||||
val file = path.toFile()
|
||||
val dirs = file.listFiles(filter)
|
||||
|
||||
return dirs ?: arrayOf()
|
||||
}
|
||||
}
|
|
@ -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<File>
|
||||
}
|
|
@ -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<File> {
|
||||
return manager.listFiles(Paths.get(path), filter)
|
||||
}
|
||||
}
|
205
app/src/main/java/be/re/writand/screens/components/PopUps.kt
Normal file
205
app/src/main/java/be/re/writand/screens/components/PopUps.kt
Normal file
|
@ -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?")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
"<No path selected yet>"
|
||||
}
|
||||
|
||||
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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<File>) : 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
|
||||
}
|
|
@ -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>(DirectoryProviderUiState.Empty)
|
||||
val uiState: StateFlow<DirectoryProviderUiState> = _uiState
|
||||
|
||||
val currentPath = mutableStateListOf(root)
|
||||
val selectedItem = mutableStateOf<String?>(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()
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Uri?> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package be.re.writand.utils.directorypicker
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
interface IDirectoryPickerListener {
|
||||
fun notify(uri: Uri)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM19,14h-3v3h-2v-3h-3v-2h3L14,9h2v3h3v2z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/baseline_download_16.xml
Normal file
5
app/src/main/res/drawable/baseline_download_16.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/baseline_home_16.xml
Normal file
5
app/src/main/res/drawable/baseline_home_16.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/baseline_image_16.xml
Normal file
5
app/src/main/res/drawable/baseline_image_16.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="16dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/baseline_library_music_16.xml
Normal file
5
app/src/main/res/drawable/baseline_library_music_16.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,7h-3v5.5c0,1.38 -1.12,2.5 -2.5,2.5S10,13.88 10,12.5s1.12,-2.5 2.5,-2.5c0.57,0 1.08,0.19 1.5,0.51L14,5h4v2zM4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/baseline_video_file_16.xml
Normal file
5
app/src/main/res/drawable/baseline_video_file_16.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M14,2H6.01c-1.1,0 -2,0.89 -2,2L4,20c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM13,9V3.5L18.5,9H13zM14,14l2,-1.06v4.12L14,16v1c0,0.55 -0.45,1 -1,1H9c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1h4c0.55,0 1,0.45 1,1V14z"/>
|
||||
|
||||
</vector>
|
5
app/src/main/res/drawable/round_folder_16.xml
Normal file
5
app/src/main/res/drawable/round_folder_16.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10.59,4.59C10.21,4.21 9.7,4 9.17,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-1.41,-1.41z"/>
|
||||
|
||||
</vector>
|
Loading…
Reference in a new issue