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.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
@ -21,14 +20,11 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import be.re.writand.navigation.WNavGraph
|
import be.re.writand.navigation.WNavGraph
|
||||||
import be.re.writand.screens.filetree.WFiletree
|
|
||||||
import be.re.writand.ui.theme.WritandTheme
|
import be.re.writand.ui.theme.WritandTheme
|
||||||
import be.re.writand.utils.directorypicker.DirectoryPicker
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val picker = DirectoryPicker(this)
|
|
||||||
private val EXTERNAL_STORAGE_PERMISSION_CODE = 100
|
private val EXTERNAL_STORAGE_PERMISSION_CODE = 100
|
||||||
private val EXTERNAL_STORAGE_PERMISSION = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
|
private val EXTERNAL_STORAGE_PERMISSION = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
|
||||||
private var hasPermission = false
|
private var hasPermission = false
|
||||||
|
|
|
@ -5,6 +5,9 @@ import be.re.writand.utils.GenerateId
|
||||||
import be.re.writand.utils.WFileTreeAbstraction
|
import be.re.writand.utils.WFileTreeAbstraction
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
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.Files
|
||||||
import java.nio.file.NotDirectoryException
|
import java.nio.file.NotDirectoryException
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
@ -138,4 +141,11 @@ class FileManagerLocal @Inject constructor(
|
||||||
val file = path.toFile()
|
val file = path.toFile()
|
||||||
return file.readLines().joinToString(separator = "\n")
|
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
|
package be.re.writand.data.local.filemanager
|
||||||
|
|
||||||
import be.re.writand.utils.WFileTreeAbstraction
|
import be.re.writand.utils.WFileTreeAbstraction
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileFilter
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.file.InvalidPathException
|
import java.nio.file.InvalidPathException
|
||||||
import java.nio.file.NotDirectoryException
|
import java.nio.file.NotDirectoryException
|
||||||
|
@ -65,4 +67,11 @@ interface IFileManager {
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
suspend fun read(path: Path): String
|
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.height
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.AbsoluteAlignment
|
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[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[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[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
|
@Composable
|
||||||
fun WPopup(
|
fun WPopup(
|
||||||
|
@ -40,6 +43,8 @@ fun WPopup(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
bottomBar: (@Composable () -> Unit) = {},
|
bottomBar: (@Composable () -> Unit) = {},
|
||||||
|
floatingButton: @Composable () -> Unit = {},
|
||||||
|
floatingButtonPosition: FabPosition = FabPosition.End,
|
||||||
children: @Composable (PaddingValues) -> Unit = {}
|
children: @Composable (PaddingValues) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Dialog(
|
Dialog(
|
||||||
|
@ -56,6 +61,8 @@ fun WPopup(
|
||||||
topBar = titleBar,
|
topBar = titleBar,
|
||||||
bottomBar = bottomBar,
|
bottomBar = bottomBar,
|
||||||
content = children,
|
content = children,
|
||||||
|
floatingActionButton = floatingButton,
|
||||||
|
floatingActionButtonPosition = floatingButtonPosition,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.border(
|
.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 androidx.compose.ui.unit.dp
|
||||||
import be.re.writand.data.local.filemanager.FileType
|
import be.re.writand.data.local.filemanager.FileType
|
||||||
import be.re.writand.screens.WUIGlobals
|
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.WBorderButton
|
||||||
import be.re.writand.screens.components.WButton
|
import be.re.writand.screens.components.WButton
|
||||||
import be.re.writand.screens.components.WCheckbox
|
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) },
|
hidePopUp = { setShowAddPopUp(false) },
|
||||||
onConfirm = { name, type -> onCreate(root, name, type) }
|
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