feat: rename and move file/directory support in filetree

This commit is contained in:
Robin Meersman 2024-11-24 18:34:48 +00:00
parent 7e97efe523
commit 96456b0fb8
11 changed files with 350 additions and 244 deletions

View file

@ -5,7 +5,6 @@ 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

View file

@ -1,183 +0,0 @@
package be.re.writand.screens.components
import android.util.Log
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
/**
* Currently this is unused, but the goal is to implement this feature into the filetree.
* based on: [this video](https://www.youtube.com/watch?v=ST99k8qK6SM)
*/
internal class DragTargetInfo {
var isDragging: Boolean by mutableStateOf(false)
var dragPosition by mutableStateOf(Offset.Zero)
var dragOffset by mutableStateOf(Offset.Zero)
var draggableComposable by mutableStateOf<(@Composable () -> Unit)?>(null)
var dataToDrop by mutableStateOf<Any?>(null)
}
internal val LocalDragTargetInfo = compositionLocalOf { DragTargetInfo() }
@Composable
fun <T> DragTarget(
modifier: Modifier = Modifier,
dataToDrop: T,
onStartDragging: () -> Unit,
onStopDragging: () -> Unit,
content: @Composable () -> Unit
) {
var currentPosition by remember { mutableStateOf(Offset.Zero) }
val currentState = LocalDragTargetInfo.current
Box(
modifier = modifier
.onGloballyPositioned {
currentPosition = it.windowToLocal(Offset.Zero)
}
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
onStartDragging()
currentState.dataToDrop = dataToDrop
currentState.isDragging = true
currentState.dragPosition = currentPosition + it
currentState.draggableComposable = content
Log.d("DRAGGING", "states has been set")
},
onDrag = { change, dragAmount ->
change.consume()
currentState.dragOffset += dragAmount
},
onDragEnd = {
onStopDragging()
currentState.dragPosition = Offset.Zero
currentState.isDragging = false
},
onDragCancel = {
onStopDragging()
currentState.dragPosition = Offset.Zero
currentState.isDragging = false
}
)
}
) {
content()
}
}
@Composable
fun <T> DropItem(
modifier: Modifier = Modifier,
content: @Composable (BoxScope.(isInBound: Boolean, data: T?) -> Unit)
) {
val dragInfo = LocalDragTargetInfo.current
val dragPosition = dragInfo.dragPosition
val dragOffset = dragInfo.dragOffset
var isCurrentDropTarget by remember { mutableStateOf(false) }
Box(
modifier = modifier.onGloballyPositioned {
it.boundsInWindow().let { rect ->
isCurrentDropTarget = rect.contains(dragPosition + dragOffset)
}
}
) {
val data =
if (isCurrentDropTarget && !dragInfo.isDragging) dragInfo.dataToDrop as T? else null
content(isCurrentDropTarget, data)
}
}
@Composable
fun DraggableScreen(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit
) {
val state = remember { DragTargetInfo() }
CompositionLocalProvider(
LocalDragTargetInfo provides state
) {
Box(modifier = modifier.fillMaxSize()) {
content()
if (state.isDragging) {
var targetSize by remember { mutableStateOf(IntSize.Zero) }
Box(modifier = Modifier
.graphicsLayer {
val offset = state.dragPosition + state.dragOffset
scaleX = 1.3f
scaleY = 1.3f
alpha = if (targetSize == IntSize.Zero) 0f else .9f
translationX = offset.x.minus(targetSize.width / 2)
translationY = offset.y.minus(targetSize.height / 2)
}
.onGloballyPositioned {
targetSize = it.size
}) {
state.draggableComposable?.invoke()
}
}
}
}
}
@Composable
fun DraggableLazyColumnScreen(
modifier: Modifier = Modifier,
content: LazyListScope.() -> Unit
) {
val state = remember { DragTargetInfo() }
CompositionLocalProvider(
LocalDragTargetInfo provides state
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(10.dp)
) {
content()
}
if(state.isDragging) {
var targetSize by remember { mutableStateOf(IntSize.Zero) }
Box(
modifier = Modifier
.zIndex(1.0f)
.graphicsLayer {
val offset = state.dragPosition + state.dragOffset - Offset(0f, listState.firstVisibleItemScrollOffset.toFloat())
alpha = if (targetSize == IntSize.Zero) 0f else .9f
translationX = offset.x - targetSize.width / 2
translationY = offset.y - targetSize.height / 2
}
.onGloballyPositioned {
Log.d("DRAGGING", "original targetsize = $targetSize, new value = ${it.size}")
targetSize = it.size
}) {
state.draggableComposable?.invoke()
}
}
}
}

View file

@ -203,3 +203,60 @@ fun RemoveFilePopUp(filename: String, hidePopUp: () -> Unit, onConfirm: () -> Un
}
}
}
@Composable
fun RenameFilePopUp(oldName: String, hidePopUp: () -> Unit, onConfirm: (String) -> Unit) {
val (newName, setNewName) = remember { mutableStateOf(oldName) }
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
)
WButton(
text = "Rename",
onClick = {
onConfirm(newName)
hidePopUp()
}
)
}
},
height = 200.dp,
width = 500.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 = newName,
onTextChange = { setNewName(it) }
)
}
}
}
}

View file

@ -23,14 +23,13 @@ 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.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -54,6 +53,11 @@ import be.re.writand.screens.components.WPopup
import be.re.writand.screens.components.WText
import be.re.writand.ui.theme.MainGreen
enum class WProviderType {
OpenDirectoryProvider,
MoveFileProvider
}
@Composable
private fun TopBar(onDismiss: () -> Unit, onBack: () -> Unit) {
Row(
@ -167,9 +171,15 @@ private fun ShortcutColumnItem(name: String, icon: Int, onClick: () -> Unit) {
@Composable
fun DirectoryProvider(
vm: DirectoryProviderViewModel = hiltViewModel(),
type: WProviderType = WProviderType.OpenDirectoryProvider,
root: String = "/storage/emulated/0",
onDismiss: () -> Unit,
onConfirm: (String) -> Unit
) {
LaunchedEffect(Unit) {
vm.initProvider(root)
}
// 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),

View file

@ -1,5 +1,6 @@
package be.re.writand.screens.directoryprovider
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import be.re.writand.data.local.filemanager.FileType
@ -24,18 +25,22 @@ class DirectoryProviderViewModel @Inject constructor(
private val listDirectoriesUseCase: ListDirectoriesUseCase,
private val createFileUseCase: CreateFileUseCase
) : WViewModel() {
private val root = "/storage/emulated/0"
private lateinit var root: String
val separator: String = File.separator
private val _uiState =
MutableStateFlow<DirectoryProviderUiState>(DirectoryProviderUiState.Empty)
val uiState: StateFlow<DirectoryProviderUiState> = _uiState
val currentPath = mutableStateListOf(root)
val currentPath = mutableStateListOf<String>()
val selectedItem = mutableStateOf<String?>(null)
init {
listCurrentDirectory()
fun initProvider(path: String = "/storage/emulated/0") {
if (!this::root.isInitialized) {
root = path
currentPath.add(root)
listCurrentDirectory()
}
}
fun createNewDirectory(name: String) {
@ -73,7 +78,7 @@ class DirectoryProviderViewModel @Inject constructor(
fun moveUp() {
if (currentPath.size > 1) {
currentPath.removeLast()
currentPath.removeAt(currentPath.lastIndex)
selectedItem.value = null
listCurrentDirectory()
}

View file

@ -2,19 +2,13 @@ package be.re.writand.screens.filetree
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.Column
import androidx.compose.foundation.layout.Row
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.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
@ -144,8 +138,10 @@ fun WFiletree(
isDirectory = { id -> vm.isDirectory(id) },
getIsOpened = { id -> vm.getIsOpened(id) },
getPath = { id -> vm.getPath(id) },
findNode = { p -> vm.findNode(p) },
projectRoot = root,
toggleIsOpened = { id -> vm.toggleOpen(id) },
onMove = { from, to -> vm.moveFile(from, to) },
onMove = { from, to, name -> vm.moveFile(from, to, name) },
onCreate = { node, name, type -> vm.createFile(name, type, node) },
onDelete = { node -> vm.removeFile(node) },
onSelect = onSelect

View file

@ -100,16 +100,24 @@ class WFiletreeViewModel @Inject constructor(
}
}
fun moveFile(from: FileNode, to: FileNode) {
fun moveFile(from: FileNode, to: FileNode, name: String) {
if (!this::fileTree.isInitialized) return
val fromPath = fileTree.getPath(from.item)!!
val toPath = fileTree.getPath(to.item)!!
val toPath = if (from != to) {
Paths.get(fileTree.getPath(to.item)!!.toString(), name)
} else {
Paths.get(fileTree.getPath(from.item)!!.parent.toString(), name)
}
fileTree.move(from, to)
fileTree.move(from, to, name)
launchCatching {
withContext(Dispatchers.IO) {
moveFileUseCase(fromPath, toPath)
}
}
}
fun findNode(path: String): FileNode? {
return fileTree.find(Paths.get(path))
}
}

View file

@ -1,47 +1,52 @@
package be.re.writand.screens.filetree
import androidx.compose.foundation.background
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.PaddingValues
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.HorizontalDivider
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import be.re.writand.R
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
import be.re.writand.screens.components.WDangerButton
import be.re.writand.screens.components.WLabelAndTextField
import be.re.writand.screens.components.RenameFilePopUp
import be.re.writand.screens.components.WPopup
import be.re.writand.screens.components.WText
import be.re.writand.screens.directoryprovider.DirectoryProvider
import be.re.writand.screens.directoryprovider.WProviderType
import be.re.writand.utils.FileNode
import java.nio.file.Path
// TODO: add drag and drop functionality to move files from 1 place to another
// based on: https://stackoverflow.com/a/71709816
/**
@ -52,6 +57,7 @@ import java.nio.file.Path
* @param[isDirectory] a function to determine if the node stands for a file or a directory.
* @param[getIsOpened] a function to access the state for a given node representing a directory.
* @param[getPath] a function to retrieve the path of a node.
* @param[findNode] a function to search the filetree for the node corresponding to a given path.
* @param[toggleIsOpened] the state changer function, toggles between opened and closed.
* @param[onMove] the action function to move a file / directory from a to b.
* @param[onCreate] the action function to create a new file / directory.
@ -61,13 +67,15 @@ import java.nio.file.Path
@Composable
fun WTreeComponent(
root: FileNode,
projectRoot: String,
modifier: Modifier = Modifier,
getFilename: (ULong) -> String?,
isDirectory: (ULong) -> Boolean,
getIsOpened: (ULong) -> Boolean,
getPath: (ULong) -> Path?,
findNode: (String) -> FileNode?,
toggleIsOpened: (ULong) -> Unit,
onMove: (FileNode, FileNode) -> Unit,
onMove: (FileNode, FileNode, String) -> Unit,
onCreate: (FileNode, String, FileType) -> Unit,
onDelete: (FileNode) -> Unit,
onSelect: (Path?) -> Unit
@ -78,6 +86,8 @@ fun WTreeComponent(
depth = 0,
getFilename = getFilename,
getPath = getPath,
findNode = findNode,
projectRoot = projectRoot,
isDirectory = isDirectory,
getIsOpened = getIsOpened,
toggleIsOpened = toggleIsOpened,
@ -100,10 +110,12 @@ fun LazyListScope.WTreeItems(
depth: Int,
getFilename: (ULong) -> String?,
getPath: (ULong) -> Path?,
findNode: (String) -> FileNode?,
projectRoot: String,
isDirectory: (ULong) -> Boolean,
getIsOpened: (ULong) -> Boolean,
toggleIsOpened: (ULong) -> Unit,
onMove: (FileNode, FileNode) -> Unit,
onMove: (FileNode, FileNode, String) -> Unit,
onCreate: (FileNode, String, FileType) -> Unit,
onDelete: (FileNode) -> Unit,
onSelect: (Path?) -> Unit
@ -114,6 +126,8 @@ fun LazyListScope.WTreeItems(
depth = depth,
getFilename = getFilename,
getPath = getPath,
findNode = findNode,
projectRoot = projectRoot,
isDirectory = isDirectory,
getIsOpened = getIsOpened,
toggleIsOpened = toggleIsOpened,
@ -135,10 +149,12 @@ fun LazyListScope.WTreeItem(
depth: Int,
getFilename: (ULong) -> String?,
getPath: (ULong) -> Path?,
findNode: (String) -> FileNode?,
projectRoot: String,
isDirectory: (ULong) -> Boolean,
getIsOpened: (ULong) -> Boolean,
toggleIsOpened: (ULong) -> Unit,
onMove: (FileNode, FileNode) -> Unit,
onMove: (FileNode, FileNode, String) -> Unit,
onCreate: (FileNode, String, FileType) -> Unit,
onDelete: (FileNode) -> Unit,
onSelect: (Path?) -> Unit
@ -149,6 +165,10 @@ fun LazyListScope.WTreeItem(
item {
val (showAddPopUp, setShowAddPopUp) = remember { mutableStateOf(false) }
val (showRemovePopUp, setShowRemovePopUp) = remember { mutableStateOf(false) }
val (isShowMore, setIsShowMore) = remember { mutableStateOf(false) }
val (showMovePopUp, setShowMovePopUp) = remember { mutableStateOf(false) }
val (showRenamePopUp, setShowRenamePopUp) = remember { mutableStateOf(false) }
Row {
Filename(
root = root,
@ -160,32 +180,151 @@ fun LazyListScope.WTreeItem(
toggleIsOpened = toggleIsOpened,
onSelect = onSelect
)
if (isDir) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add a file or directory",
modifier = Modifier
.size(WUIGlobals.treeIconSize)
.clickable {
setShowAddPopUp(true)
}
)
}
Spacer(modifier = Modifier.width(10.dp))
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete the current file or directory",
painter = painterResource(R.drawable.baseline_more_horiz_16),
contentDescription = "Show more",
modifier = Modifier
.size(WUIGlobals.treeIconSize)
.clickable {
setShowRemovePopUp(true)
setIsShowMore(true)
}
)
if (isShowMore) WPopup(
width = 300.dp,
height = 200.dp,
onDismiss = { setIsShowMore(false) },
titleBar = { /* Left empty on purpose */ }
) { padding ->
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(10.dp)
) {
if (isDir) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 0.dp, vertical = 10.dp)
.clickable {
setIsShowMore(false)
setShowAddPopUp(true)
}
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add a file/directory",
modifier = Modifier
.size(WUIGlobals.iconSize)
)
WText(
text = "Add a file/directory",
modifier = Modifier
.padding(horizontal = 5.dp)
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 0.dp, vertical = 10.dp)
.clickable {
setIsShowMore(false)
setShowMovePopUp(true)
}
) {
Icon(
painter = painterResource(R.drawable.baseline_drive_file_move_16),
contentDescription = "Move file/directory",
modifier = Modifier
.size(WUIGlobals.iconSize)
)
WText(
text = "Move $filename",
modifier = Modifier
.padding(horizontal = 5.dp)
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 0.dp, vertical = 10.dp)
.clickable {
setIsShowMore(false)
setShowRenamePopUp(true)
}
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Rename file/directory",
modifier = Modifier
.size(WUIGlobals.iconSize)
)
WText(
text = "Rename $filename",
modifier = Modifier
.padding(horizontal = 5.dp)
)
}
Column {
HorizontalDivider(
modifier = Modifier.padding(
horizontal = 0.dp,
vertical = 5.dp
)
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(WUIGlobals.cornerRadius))
.background(Color.Red)
.padding(horizontal = 5.dp, vertical = 10.dp)
.clickable {
setIsShowMore(false)
setShowRemovePopUp(true)
}
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
modifier = Modifier
.size(WUIGlobals.iconSize)
)
WText(
text = "Delete $filename",
color = Color.White,
modifier = Modifier.padding(horizontal = 5.dp)
)
}
}
}
}
}
if (showRenamePopUp) RenameFilePopUp(
oldName = filename,
hidePopUp = { setShowRenamePopUp(false) },
onConfirm = { name -> onMove(root, root, name) }
)
if (showAddPopUp) CreateFilePopUp(
type = CreationType.ALL,
hidePopUp = { setShowAddPopUp(false) },
onConfirm = { name, type -> onCreate(root, name, type) }
onConfirm = { name, type -> onCreate(root, name, type) },
type = CreationType.ALL
)
if (showRemovePopUp) RemoveFilePopUp(
@ -193,6 +332,20 @@ fun LazyListScope.WTreeItem(
hidePopUp = { setShowRemovePopUp(false) },
onConfirm = { onDelete(root) }
)
if (showMovePopUp) DirectoryProvider(
type = WProviderType.MoveFileProvider,
root = projectRoot,
onDismiss = { setShowMovePopUp(false) }
) { result ->
// find goal node in tree according to result:
val goal = findNode(result)
goal?.let {
getFilename(root.item)?.let { filename -> onMove(root, it, filename) }
}
setShowMovePopUp(false)
}
}
if (getIsOpened(root.item)) {
WTreeItems(
@ -200,6 +353,8 @@ fun LazyListScope.WTreeItem(
depth = depth + 1,
getFilename = getFilename,
getPath = getPath,
findNode = findNode,
projectRoot = projectRoot,
isDirectory = isDirectory,
getIsOpened = getIsOpened,
toggleIsOpened = toggleIsOpened,

View file

@ -1,16 +1,12 @@
package be.re.writand.utils
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.snapshots.MutableSnapshot
import java.nio.file.Path
import java.nio.file.Paths
import java.util.AbstractQueue
import java.util.LinkedList
import java.util.PriorityQueue
import java.util.Queue
import javax.inject.Inject
import kotlin.io.path.isDirectory
import kotlin.io.path.name
@ -33,10 +29,36 @@ class WFileTreeAbstraction @Inject constructor(
_root.value = _root.value
}
/**
* Update all the children after a directory is renamed. Makes sure this change
* is reflected in the index.
* @param[node] the node which is renamed.
* @param[oldName] the old name of the node.
* @param[newName] the new name of the node.
*/
private fun renameChildren(node: FileNode, oldName: String, newName: String) {
val queue = LinkedList<FileNode>()
queue.addAll(node.children)
while (queue.isNotEmpty()) {
val top = queue.poll()!!
index[top.item] = Paths.get(index[top.item]!!.toString().replace(oldName, newName))
if (top.children.isNotEmpty()) {
queue.addAll(top.children)
}
}
}
private val comparator = compareBy<FileNode> { index[it.item]?.isDirectory() }
.reversed()
.thenBy { index[it.item]?.name }
/**
* Gets the path according to [id].
* @param[id] the id of the node.
* @return the path of the node.
*/
fun getPath(id: ULong): Path? {
return index[id]
}
@ -53,7 +75,7 @@ class WFileTreeAbstraction @Inject constructor(
val queue = LinkedList<FileNode>()
queue.add(node)
while(!queue.isEmpty()) {
while (!queue.isEmpty()) {
val top = queue.poll()!!
queue.addAll(top.children)
index.remove(top.item)
@ -78,26 +100,53 @@ class WFileTreeAbstraction @Inject constructor(
/**
* Move a file from 1 location to another.
* If [from] and [to] are equal, a renaming operation took place
* @param[from] the source location to be moved.
* @param[to] the destination directory.
*/
fun move(from: FileNode, to: FileNode) {
from.parent?.children?.remove(from)
to.children.add(from)
from.parent = to
to.children.sortWith(comparator)
fun move(from: FileNode, to: FileNode, name: String) {
val oldName = index[from.item]!!.fileName ?: throw IllegalArgumentException()
if (from != to) {
from.parent?.children?.remove(from)
to.children.add(from)
from.parent = to
to.children.sortWith(comparator)
// update the path stored in the index
val parentPath = index[to.item] ?: throw IllegalArgumentException()
val fromPath = index[from.item] ?: throw IllegalArgumentException()
index[from.item] = Paths.get(parentPath.toString(), fromPath.name)
// update the path stored in the index
val parentPath = index[to.item] ?: throw IllegalArgumentException()
index[from.item] = Paths.get(parentPath.toString(), name)
} else {
val path = index[from.item] ?: throw IllegalArgumentException()
index[from.item] = Paths.get(path.parent.toString(), name)
}
if (from.children.isNotEmpty()) renameChildren(from, oldName.toString(), name)
triggerRerender()
}
/**
* Find a node according to a given path.
* @param[path] the path to find.
* @return null if no node is found, or the node corresponding to [path].
*/
fun find(path: Path): FileNode? {
var currentNode: FileNode? = _root.value
while (currentNode != null) {
val currentPath = index[currentNode.item]
if (currentPath == path) return currentNode
currentNode = currentNode.children.find {
path.contains(index[it.item]?.fileName)
}
}
return null
}
override var value: FileNode
get() = _root.value
set(value) {_root.value = value}
set(value) {
_root.value = value
}
override fun component1(): FileNode {
return _root.value

View file

@ -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="M20,6h-8l-2,-2L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM14,18v-3h-4v-4h4L14,8l5,5 -5,5z"/>
</vector>

View 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="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>