forked from Writand/writand
feat: rename and move file/directory support in filetree
This commit is contained in:
parent
7e97efe523
commit
96456b0fb8
11 changed files with 350 additions and 244 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
5
app/src/main/res/drawable/baseline_more_horiz_16.xml
Normal file
5
app/src/main/res/drawable/baseline_more_horiz_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="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>
|
Loading…
Reference in a new issue