diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 0000000..a5fcdd9
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,26 @@
+name: Android CI
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ - name: Build with Gradle
+ run: ./gradlew build
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 0fc3113..e805548 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 3d8b933..5fc4159 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,6 @@
plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.android'
+ alias libs.plugins.android.application
+ alias libs.plugins.kotlin.android
}
android {
@@ -11,8 +11,8 @@ android {
applicationId "com.reysand.files"
minSdk 33
targetSdk 33
- versionCode 1
- versionName "0.0.1"
+ versionCode 3
+ versionName "0.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -37,7 +37,7 @@ android {
compose true
}
composeOptions {
- kotlinCompilerExtensionVersion '1.4.3'
+ kotlinCompilerExtensionVersion '1.5.4'
}
packaging {
resources {
@@ -48,19 +48,24 @@ android {
dependencies {
- implementation 'androidx.core:core-ktx:1.9.0'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
- implementation 'androidx.activity:activity-compose:1.7.2'
- implementation platform('androidx.compose:compose-bom:2023.03.00')
- implementation 'androidx.compose.ui:ui'
- implementation 'androidx.compose.ui:ui-graphics'
- implementation 'androidx.compose.ui:ui-tooling-preview'
- implementation 'androidx.compose.material3:material3'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
- androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00')
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
- debugImplementation 'androidx.compose.ui:ui-tooling'
- debugImplementation 'androidx.compose.ui:ui-test-manifest'
+ implementation libs.core.ktx
+ implementation libs.lifecycle.runtime.ktx
+ implementation libs.activity.compose
+
+ implementation platform(libs.compose.bom)
+ implementation libs.ui
+ implementation libs.ui.graphics
+ implementation libs.ui.tooling.preview
+ implementation libs.material3
+ implementation libs.lifecycle.viewmodel.compose
+ implementation libs.navigation.compose
+
+ testImplementation libs.junit
+ androidTestImplementation libs.androidx.test.ext.junit
+ androidTestImplementation libs.espresso.core
+ androidTestImplementation platform(libs.compose.bom)
+ androidTestImplementation libs.ui.test.junit4
+
+ debugImplementation libs.ui.tooling
+ debugImplementation libs.ui.test.manifest
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f469594..849ef44 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,9 +13,16 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+
+
+
+
{
+ // List files in the specified path
+ val files = File(path).listFiles()
+
+ // Separate directories and other files
+ val directories = mutableListOf()
+ val others = mutableListOf()
+
+ files?.forEach { file ->
+ val fileModel = createFileModel(file)
+ when (fileModel.fileType) {
+ FileModel.FileType.DIRECTORY -> directories.add(fileModel)
+ else -> others.add(fileModel)
+ }
+ }
+
+ // Sort directories and other files separately
+ directories.sortBy { it.name.lowercase() }
+ others.sortBy { it.name.lowercase() }
+
+ // Combine and return the sorted list
+ return directories + others
+ }
+
+ override suspend fun moveFile(source: String, destination: String): Boolean {
+ return renameFile(source, destination)
+ }
+
+ override suspend fun copyFile(source: String, destination: String): Boolean {
+ return operateOnFile(source, destination) { sourceFile, destinationFile ->
+ sourceFile.copyRecursively(destinationFile, true)
+ }
+ }
+
+ override suspend fun renameFile(from: String, to: String): Boolean {
+ return operateOnFile(from, to) { sourceFile, destinationFile ->
+ sourceFile.renameTo(destinationFile)
+ }
+ }
+
+ override suspend fun deleteFile(path: String): Boolean {
+ return File(path).deleteRecursively()
+ }
+
+ /**
+ * Creates a [FileModel] object from a [File] instance.
+ *
+ * @param file The [File] instance to create a [FileModel] from.
+ * @return A [FileModel] object representing the given file.
+ */
+ private fun createFileModel(file: File): FileModel {
+ return FileModel(
+ name = file.name,
+ path = file.path,
+ fileType = when (file.isDirectory) {
+ true -> FileModel.FileType.DIRECTORY
+ else -> FileModel.FileType.OTHER
+ },
+ size = file.length(),
+ lastModified = file.lastModified()
+ )
+ }
+
+ /**
+ * Performs an operation on a file.
+ *
+ * @param source The path of the source file.
+ * @param destination The path of the destination file.
+ * @param operation The operation to perform on the file.
+ * @return A boolean value indicating the success of the operation.
+ */
+ private fun operateOnFile(
+ source: String,
+ destination: String,
+ operation: (File, File) -> Boolean
+ ): Boolean {
+ return try {
+ val sourceFile = File(source)
+ val destinationFile = File(destination)
+
+ if (operation(sourceFile, destinationFile)) {
+ createFileModel(destinationFile)
+ true
+ } else {
+ false
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/data/model/FileModel.kt b/app/src/main/java/com/reysand/files/data/model/FileModel.kt
new file mode 100644
index 0000000..e8ee60c
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/data/model/FileModel.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.data.model
+
+import com.reysand.files.data.util.FileSizeFormatter
+import java.util.Calendar
+import java.util.Locale
+
+/**
+ * Data class representing a file.
+ *
+ * @property name The name of the file.
+ * @property path The path of the file.
+ * @property fileType The type of the file.
+ * @property size The size of the file in bytes.
+ * @property lastModified The timestamp of the last modification.
+ */
+data class FileModel(
+ val name: String,
+ val path: String,
+ val fileType: FileType,
+ val size: Long,
+ val lastModified: Long
+) {
+ /**
+ * Enum class representing the type of file.
+ */
+ enum class FileType {
+ DIRECTORY,
+ OTHER
+ }
+
+ /**
+ * Gets a human-readable formatted size.
+ *
+ * @return A string representing the formatted size (e.g., "2.5 MB").
+ */
+ fun getFormattedSize(): String {
+ return FileSizeFormatter.getFormattedSize(size)
+ }
+
+ /**
+ * Gets the last modified date in a human-readable format.
+ *
+ * @return A string representing the last modified date (e.g., "Sep 12, 2023).
+ */
+ fun getLastModified(): String {
+ val current = Calendar.getInstance()
+ val date = Calendar.getInstance().apply { timeInMillis = lastModified }
+
+ val displayMonth = date.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault())
+ val dayOfMonth = date.get(Calendar.DAY_OF_MONTH)
+
+ val formattedDate = buildString {
+ append("$displayMonth $dayOfMonth")
+ if (date.get(Calendar.YEAR) != current.get(Calendar.YEAR)) {
+ append(", ${date.get(Calendar.YEAR)}")
+ }
+ }
+ return formattedDate
+ }
+}
diff --git a/app/src/main/java/com/reysand/files/data/repository/FileRepository.kt b/app/src/main/java/com/reysand/files/data/repository/FileRepository.kt
new file mode 100644
index 0000000..9f6b883
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/data/repository/FileRepository.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.data.repository
+
+import com.reysand.files.data.model.FileModel
+
+/**
+ * Interface defining operations for interacting with files.
+ */
+interface FileRepository {
+
+ /**
+ * Retrieves the free space of the storage.
+ *
+ * @return A formatted string containing the free space of the storage.
+ */
+ fun getStorageFreeSpace(): String
+
+ /**
+ * Retrieves a list of [FileModel] objects from the specified path.
+ *
+ * @param path The path to the directory containing the files.
+ * @return A list of [FileModel] objects.
+ */
+ suspend fun getFiles(path: String): List
+
+ /**
+ * Move a file from one path to another.
+ *
+ * @param source The original path of the file.
+ * @param destination The new path of the file.
+ * @return A boolean value indicating the success of the operation.
+ */
+ suspend fun moveFile(source: String, destination: String): Boolean
+
+ /**
+ * Copy a file from one path to another.
+ *
+ * @param source The original path of the file.
+ * @param destination The new path of the file.
+ * @return A boolean value indicating the success of the operation.
+ */
+ suspend fun copyFile(source: String, destination: String): Boolean
+
+ /**
+ * Rename a file.
+ *
+ * @param from The original path of the file.
+ * @param to The new path of the file.
+ * @return A boolean value indicating the success of the operation.
+ */
+ suspend fun renameFile(from: String, to: String): Boolean
+
+ /**
+ * Delete a file.
+ *
+ * @param path The path of the file to delete.
+ * @return A boolean value indicating the success of the operation.
+ */
+ suspend fun deleteFile(path: String): Boolean
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/data/util/FileSizeFormatter.kt b/app/src/main/java/com/reysand/files/data/util/FileSizeFormatter.kt
new file mode 100644
index 0000000..a6573de
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/data/util/FileSizeFormatter.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.data.util
+
+import kotlin.math.log10
+import kotlin.math.pow
+
+/**
+ * Utility class for formatting file sizes.
+ */
+object FileSizeFormatter {
+
+ /**
+ * Formats the given size in bytes to a human-readable format.
+ *
+ * @param size The size in bytes.
+ * @return A string representing the formatted size (e.g., "2.5 MB").
+ */
+ fun getFormattedSize(size: Long): String {
+ val units = arrayOf("B", "KB", "MB", "GB", "TB")
+ val base = 1024.0
+
+ val unitIndex = (log10(size.toDouble()) / log10(base)).toInt().coerceIn(0, units.size - 1)
+ val convertedSize = size / base.pow(unitIndex.toDouble())
+
+ return String.format("%.2f %s", convertedSize, units[unitIndex])
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/FilesApp.kt b/app/src/main/java/com/reysand/files/ui/FilesApp.kt
new file mode 100644
index 0000000..6733602
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/FilesApp.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.rounded.Settings
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.reysand.files.R
+import com.reysand.files.ui.components.PathTabs
+import com.reysand.files.ui.navigation.Destinations
+import com.reysand.files.ui.navigation.NavGraph
+import com.reysand.files.ui.viewmodel.FilesViewModel
+
+/**
+ * Composable function representing the main UI of the files application.
+ *
+ * @param filesViewModel The [FilesViewModel] providing data for the screen.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun FilesApp(filesViewModel: FilesViewModel = viewModel(factory = FilesViewModel.Factory)) {
+ // Create a navigation controller
+ val navController = rememberNavController()
+
+ // Get the current route from the navigation stack
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentRoute = navBackStackEntry?.destination?.route
+
+ // Determine the title for the top app bar based on the current route
+ val topBarTitle = when (currentRoute) {
+ Destinations.FILE_LIST -> R.string.internal_storage
+ Destinations.SETTINGS -> R.string.settings_title
+ else -> R.string.app_name
+ }
+
+ Scaffold(topBar = {
+ CenterAlignedTopAppBar(
+ title = {
+ Text(
+ text = stringResource(id = topBarTitle)
+ )
+ },
+ navigationIcon = {
+ if (currentRoute != Destinations.HOME) {
+ IconButton(onClick = {
+ if (filesViewModel.currentDirectory.value != filesViewModel.homeDirectory &&
+ currentRoute == Destinations.FILE_LIST) {
+ filesViewModel.navigateUp()
+ } else {
+ navController.popBackStack()
+ }
+ }) {
+ Icon(
+ imageVector = Icons.Default.ArrowBack, contentDescription = null
+ )
+ }
+ }
+ },
+ actions = {
+ if (currentRoute == Destinations.HOME) {
+ IconButton(onClick = { navController.navigate(Destinations.SETTINGS) }) {
+ Icon(
+ imageVector = Icons.Rounded.Settings, contentDescription = null
+ )
+ }
+ }
+ },
+ colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5F)
+ ),
+ )
+ }) {
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Column {
+ if (currentRoute == Destinations.FILE_LIST) {
+ PathTabs(
+ filesViewModel.homeDirectory, filesViewModel.currentDirectory.value
+ ) { newPath ->
+ filesViewModel.getFiles(newPath)
+ }
+ }
+ NavGraph(
+ filesViewModel = filesViewModel, navController = navController
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/components/DeleteDialog.kt b/app/src/main/java/com/reysand/files/ui/components/DeleteDialog.kt
new file mode 100644
index 0000000..21f981a
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/components/DeleteDialog.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.components
+
+import android.content.res.Configuration
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.reysand.files.R
+import com.reysand.files.ui.theme.FilesTheme
+
+/**
+ * Composable function for displaying a delete dialog.
+ *
+ * @param fileName The name of the file to be deleted.
+ * @param onDelete Callback for when the delete button is clicked.
+ * @param onDismiss Callback for when the dialog is dismissed.
+ */
+@Composable
+fun DeleteDialog(
+ fileName: String,
+ onDelete: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = { onDismiss() },
+ confirmButton = {
+ DialogOutlinedButton(
+ text = stringResource(id = R.string.dialog_confirm_button),
+ icon = Icons.Default.Done,
+ onClick = { onDelete() }
+ )
+ },
+ dismissButton = {
+ DialogOutlinedButton(
+ text = stringResource(id = R.string.dialog_dismiss_button),
+ icon = Icons.Default.Close,
+ onClick = { onDismiss() }
+ )
+ },
+ title = { Text(text = stringResource(id = R.string.dialog_delete_title)) },
+ text = {
+ Text(text = stringResource(id = R.string.dialog_delete_message, fileName))
+ }
+ )
+}
+
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun DeleteDialogPreview() {
+ FilesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ DeleteDialog(
+ fileName = "test.txt",
+ onDelete = { },
+ onDismiss = { }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/components/DialogOutlinedButton.kt b/app/src/main/java/com/reysand/files/ui/components/DialogOutlinedButton.kt
new file mode 100644
index 0000000..c8dddb6
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/components/DialogOutlinedButton.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.components
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.reysand.files.R
+import com.reysand.files.ui.theme.FilesTheme
+
+/**
+ * Composable function for displaying an outlined button in a dialog.
+ *
+ * @param icon The icon to display in the button.
+ * @param text The text to display in the button.
+ * @param modifier Modifier for customizing the layout.
+ * @param enabled Whether the button is enabled or not.
+ * @param onClick The callback to be invoked when the button is clicked.
+ */
+@Composable
+fun DialogOutlinedButton(
+ icon: ImageVector,
+ text: String,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ onClick: () -> Unit
+) {
+ OutlinedButton(
+ onClick = onClick,
+ modifier = modifier.fillMaxWidth(0.48F),
+ enabled = enabled
+ ) {
+ Icon(imageVector = icon, contentDescription = null)
+ Spacer(modifier = modifier.width(8.dp))
+ Text(text = text)
+ }
+}
+
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun DialogOutlinedButtonPreview() {
+ FilesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ DialogOutlinedButton(
+ icon = Icons.Rounded.Close,
+ text = stringResource(R.string.dialog_dismiss_button)
+ ) { }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/components/FileListItem.kt b/app/src/main/java/com/reysand/files/ui/components/FileListItem.kt
new file mode 100644
index 0000000..9d2d219
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/components/FileListItem.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.components
+
+import android.content.res.Configuration
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import com.reysand.files.R
+import com.reysand.files.data.model.FileModel
+import com.reysand.files.ui.theme.FilesTheme
+
+/**
+ * Composable function for displaying a single file item.
+ *
+ * @param file The [FileModel] representing the file.
+ * @param homeDirectory The path to the home directory.
+ * @param moveOperation Callback for when the move option is clicked.
+ * @param copyOperation Callback for when the copy option is clicked.
+ * @param renameOperation Callback for when the rename option is clicked.
+ * @param deleteOperation Callback for when the delete option is clicked.
+ * @param modifier Modifier for customizing the layout.
+ * @param onClick Callback for when the item is clicked.
+ */
+@Composable
+fun FileListItem(
+ file: FileModel,
+ homeDirectory: String,
+ moveOperation: (FileModel, String) -> Unit,
+ copyOperation: (FileModel, String) -> Unit,
+ renameOperation: (FileModel, String) -> Unit,
+ deleteOperation: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit
+) {
+
+ // Determine the icon based on the file type
+ val iconResId = when (file.fileType) {
+ FileModel.FileType.DIRECTORY -> R.drawable.ic_folder
+ FileModel.FileType.OTHER -> R.drawable.ic_file
+ }
+
+ // Determine the information text based on the file type
+ val info = when (file.fileType) {
+ FileModel.FileType.DIRECTORY -> file.getLastModified()
+ else -> "${file.getFormattedSize()}, ${file.getLastModified()}"
+ }
+
+ ListItem(
+ headlineContent = {
+ Text(
+ text = file.name,
+ modifier = modifier,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1
+ )
+ },
+ modifier = modifier.clickable(onClick = onClick),
+ supportingContent = {
+ Text(
+ text = info,
+ modifier = modifier
+ )
+ },
+ leadingContent = {
+ Icon(
+ painter = painterResource(id = iconResId),
+ contentDescription = null,
+ modifier = modifier
+ )
+ },
+ trailingContent = {
+ OptionsMenu(
+ file = file,
+ homeDirectory = homeDirectory,
+ moveOperation = moveOperation,
+ copyOperation = copyOperation,
+ renameOperation = renameOperation,
+ deleteOperation = deleteOperation
+ )
+ }
+ )
+}
+
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun FileListItemPreview() {
+ FilesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Column {
+ FileListItem(
+ file = FileModel(
+ name = "Android",
+ path = "/storage/emulated/0",
+ fileType = FileModel.FileType.DIRECTORY,
+ size = 1,
+ lastModified = 1_593_689_259_000
+ ),
+ homeDirectory = "",
+ moveOperation = { _, _ -> },
+ copyOperation = { _, _ -> },
+ renameOperation = { _, _ -> },
+ deleteOperation = {}
+ ) {}
+ FileListItem(
+ file = FileModel(
+ name = "text.txt",
+ path = "/storage/emulated/0",
+ fileType = FileModel.FileType.OTHER,
+ size = 1024,
+ lastModified = 1_693_689_259_000
+ ),
+ homeDirectory = "",
+ moveOperation = { _, _ -> },
+ copyOperation = { _, _ -> },
+ renameOperation = { _, _ -> },
+ deleteOperation = {}
+ ) {}
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/components/OptionsMenu.kt b/app/src/main/java/com/reysand/files/ui/components/OptionsMenu.kt
new file mode 100644
index 0000000..aa51828
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/components/OptionsMenu.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.components
+
+import android.content.res.Configuration
+import androidx.annotation.DrawableRes
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.reysand.files.R
+import com.reysand.files.data.model.FileModel
+import com.reysand.files.ui.theme.FilesTheme
+
+/**
+ * Composable function for displaying an options menu.
+ *
+ * @param file The [FileModel] representing the file.
+ * @param homeDirectory The path to the home directory.
+ * @param moveOperation Callback for when the move option is clicked.
+ * @param copyOperation Callback for when the copy option is clicked.
+ * @param renameOperation Callback for when the rename option is clicked.
+ * @param deleteOperation Callback for when the delete option is clicked.
+ */
+@Composable
+fun OptionsMenu(
+ file: FileModel,
+ homeDirectory: String,
+ moveOperation: (FileModel, String) -> Unit,
+ copyOperation: (FileModel, String) -> Unit,
+ renameOperation: (FileModel, String) -> Unit,
+ deleteOperation: (String) -> Unit
+) {
+ var expanded by remember { mutableStateOf(false) }
+ var showDialog by remember { mutableStateOf(0) }
+
+ IconButton(onClick = { expanded = true }) {
+ Icon(imageVector = Icons.Default.MoreVert, contentDescription = null)
+ DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
+ OptionsMenuItem(
+ text = stringResource(id = R.string.options_move_to),
+ leadingIcon = R.drawable.ic_option_move
+ ) {
+ showDialog = 1
+ expanded = false
+ }
+ OptionsMenuItem(
+ text = stringResource(id = R.string.options_copy_to),
+ leadingIcon = R.drawable.ic_option_copy
+ ) {
+ showDialog = 2
+ expanded = false
+ }
+ OptionsMenuItem(
+ text = stringResource(id = R.string.options_rename),
+ leadingIcon = R.drawable.ic_option_rename
+ ) {
+ showDialog = 3
+ expanded = false
+ }
+ OptionsMenuItem(
+ text = stringResource(id = R.string.options_delete),
+ leadingIcon = R.drawable.ic_option_delete
+ ) {
+ showDialog = 4
+ expanded = false
+ }
+ }
+ }
+
+ when (showDialog) {
+ 1 -> {
+ RenameDialog(
+ dialogTitle = stringResource(id = R.string.options_move_to),
+ fileName = file.path.removePrefix(homeDirectory),
+ onConfirm = { newName ->
+ moveOperation(file, newName)
+ showDialog = 0
+ },
+ onDismiss = { showDialog = 0 }
+ )
+ }
+
+ 2 -> {
+ RenameDialog(
+ dialogTitle = stringResource(id = R.string.options_copy_to),
+ fileName = file.path.removePrefix(homeDirectory),
+ onConfirm = { newName ->
+ copyOperation(file, newName)
+ showDialog = 0
+ },
+ onDismiss = { showDialog = 0 }
+ )
+ }
+
+ 3 -> {
+ RenameDialog(
+ dialogTitle = stringResource(id = R.string.options_rename),
+ fileName = file.name,
+ onConfirm = { newName ->
+ renameOperation(file, newName)
+ showDialog = 0
+ },
+ onDismiss = { showDialog = 0 }
+ )
+ }
+
+ 4 -> {
+ DeleteDialog(
+ fileName = file.name,
+ onDelete = {
+ deleteOperation(file.path)
+ showDialog = 0
+ },
+ onDismiss = { showDialog = 0 }
+ )
+ }
+ }
+}
+
+@Composable
+fun OptionsMenuItem(
+ text: String,
+ @DrawableRes leadingIcon: Int,
+ onClick: () -> Unit
+) {
+ DropdownMenuItem(
+ text = { Text(text = text) },
+ onClick = onClick,
+ leadingIcon = {
+ Icon(painter = painterResource(id = leadingIcon), contentDescription = null)
+ }
+ )
+}
+
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun OptionsMenuItemPreview() {
+ FilesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ OptionsMenuItem(
+ stringResource(id = R.string.options_rename),
+ R.drawable.ic_option_rename
+ ) {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/components/PathTabs.kt b/app/src/main/java/com/reysand/files/ui/components/PathTabs.kt
new file mode 100644
index 0000000..dc1c5e6
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/components/PathTabs.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.components
+
+import android.content.res.Configuration
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ScrollableTabRow
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.reysand.files.ui.theme.FilesTheme
+import java.io.File
+
+/**
+ * Composable function for displaying a row of tabs representing the current directory path.
+ *
+ * @param homeDirectory The path to the home directory.
+ * @param currentDirectory The path to the current directory.
+ * @param onNavigateToDirectory Callback function for navigating to a specific directory.
+ */
+@Composable
+fun PathTabs(
+ homeDirectory: String,
+ currentDirectory: String,
+ onNavigateToDirectory: (String) -> Unit
+) {
+ // Extract the relative path from the home directory
+ val path = currentDirectory.removePrefix(homeDirectory)
+
+ // Split the path into individual components
+ val pathComponents = listOf(homeDirectory) + path.split(File.separator).filter {
+ it.isNotEmpty()
+ }
+
+ ScrollableTabRow(
+ selectedTabIndex = pathComponents.size - 1,
+ containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5F),
+ edgePadding = 0.dp
+ ) {
+ pathComponents.forEachIndexed { index, component ->
+ Tab(selected = index == pathComponents.size - 1, onClick = {
+ // Join the path components to form the new directory path
+ val newPath = pathComponents.subList(0, index + 1).joinToString(File.separator)
+ onNavigateToDirectory(newPath)
+ }, text = {
+ Text(
+ text = component,
+ overflow = TextOverflow.Ellipsis,
+ softWrap = true,
+ maxLines = 1
+ )
+ })
+ }
+ }
+}
+
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun PathTabsPreview() {
+ FilesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ PathTabs(
+ homeDirectory = "/storage/emulated/0",
+ currentDirectory = "/storage/emulated/0${File.separator}Download"
+ ) {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/components/PermissionAlertDialog.kt b/app/src/main/java/com/reysand/files/ui/components/PermissionAlertDialog.kt
new file mode 100644
index 0000000..5c75306
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/components/PermissionAlertDialog.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.components
+
+import android.content.Context
+import android.content.Intent
+import android.content.res.Configuration
+import android.net.Uri
+import android.provider.Settings
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.reysand.files.R
+import com.reysand.files.ui.theme.FilesTheme
+
+/**
+ * Composable function to display a permission alert dialog.
+ *
+ * @param showPermissionDialog State that controls whether the dialog is shown.
+ * @param modifier Modifier for customizing the layout.
+ */
+@Composable
+fun PermissionAlertDialog(
+ showPermissionDialog: MutableState,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+
+ if (showPermissionDialog.value) {
+ AlertDialog(
+ onDismissRequest = { showPermissionDialog.value = false },
+ confirmButton = {
+ TextButton(onClick = {
+ allowPermission(context)
+ showPermissionDialog.value = false
+ }) {
+ Text(text = stringResource(id = R.string.file_access_permission_confirm_button))
+ }
+ },
+ modifier = modifier,
+ dismissButton = {
+ TextButton(onClick = { showPermissionDialog.value = false }) {
+ Text(text = stringResource(id = R.string.dialog_dismiss_button))
+ }
+ },
+ icon = { Icon(imageVector = Icons.Default.Warning, contentDescription = null) },
+ title = { Text(text = stringResource(id = R.string.file_access_permission_title)) },
+ text = { Text(text = stringResource(id = R.string.file_access_permission_message)) },
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer
+ )
+ }
+}
+
+/**
+ * Utility function to open the system settings for app permission management.
+ *
+ * @param context The context of the current application.
+ */
+fun allowPermission(context: Context) {
+ val uri = Uri.parse("package:${context.packageName}")
+ val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, uri)
+ context.startActivity(intent)
+}
+
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun PermissionAlertDialogPreview() {
+ FilesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ PermissionAlertDialog(remember { mutableStateOf(true) })
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/components/RenameDialog.kt b/app/src/main/java/com/reysand/files/ui/components/RenameDialog.kt
new file mode 100644
index 0000000..a5bc427
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/components/RenameDialog.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.components
+
+import android.content.res.Configuration
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.tooling.preview.Preview
+import com.reysand.files.R
+import com.reysand.files.ui.theme.FilesTheme
+
+/**
+ * Composable function for displaying a rename dialog.
+ *
+ * @param dialogTitle The title of the dialog.
+ * @param fileName The name of the file to be renamed.
+ * @param onConfirm The callback to be invoked when the confirm button is clicked.
+ * @param onDismiss The callback to be invoked when the dialog is dismissed.
+ */
+@Composable
+fun RenameDialog(
+ dialogTitle: String,
+ fileName: String,
+ onConfirm: (String) -> Unit,
+ onDismiss: () -> Unit
+) {
+ var newName by remember { mutableStateOf(fileName) }
+ var confirmButtonEnabled by remember { mutableStateOf(false) }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ confirmButton = {
+ DialogOutlinedButton(
+ icon = Icons.Default.Done,
+ text = stringResource(id = R.string.dialog_confirm_button),
+ enabled = confirmButtonEnabled,
+ onClick = { onConfirm(newName) }
+ )
+ },
+ dismissButton = {
+ DialogOutlinedButton(
+ icon = Icons.Default.Close,
+ text = stringResource(id = R.string.dialog_dismiss_button),
+ onClick = onDismiss
+ )
+ },
+ title = { Text(text = dialogTitle) },
+ text = {
+ OutlinedTextField(
+ value = newName,
+ onValueChange = {
+ newName = it
+ confirmButtonEnabled = (newName != fileName) && newName.isNotBlank()
+ },
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ keyboardActions = KeyboardActions(onDone = { onConfirm(newName) }),
+ singleLine = true,
+ maxLines = 1
+ )
+ }
+ )
+}
+
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun RenameDialogPreview() {
+ FilesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ RenameDialog("Test", "test.txt", {}, {})
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/components/StorageCard.kt b/app/src/main/java/com/reysand/files/ui/components/StorageCard.kt
new file mode 100644
index 0000000..30c142b
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/components/StorageCard.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.components
+
+import android.content.res.Configuration
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.reysand.files.R
+import com.reysand.files.ui.theme.FilesTheme
+
+/**
+ * Composable function for displaying a storage card.
+ *
+ * @param title The title of the card.
+ * @param leadingIcon The resource ID of the leading icon.
+ * @param info Additional information to display.
+ * @param modifier Modifier for customizing the layout.
+ * @param enabled Whether the card is enabled or not.
+ * @param onClick The callback to be executed when the card is clicked.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun StorageCard(
+ title: String,
+ @DrawableRes leadingIcon: Int,
+ info: String,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ onClick: () -> Unit
+) {
+ Card(
+ onClick = onClick,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ enabled = enabled,
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
+ elevation = CardDefaults.cardElevation(2.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(id = leadingIcon),
+ contentDescription = null,
+ modifier = Modifier.size(40.dp)
+ )
+ Spacer(modifier = Modifier.size(16.dp))
+ Column {
+ Text(text = title, style = MaterialTheme.typography.titleMedium)
+ Text(text = info, style = MaterialTheme.typography.bodyMedium)
+ }
+ }
+ }
+}
+
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun StorageCardPreview() {
+ FilesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ StorageCard(
+ title = "Internal Storage",
+ enabled = true,
+ leadingIcon = R.drawable.ic_internal_storage,
+ info = "O B"
+ ) { }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/navigation/Destinations.kt b/app/src/main/java/com/reysand/files/ui/navigation/Destinations.kt
new file mode 100644
index 0000000..c6f5138
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/navigation/Destinations.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.navigation
+
+/**
+ * Object containing constant destination keys used for navigation.
+ */
+object Destinations {
+ const val HOME = "home"
+ const val FILE_LIST = "fileList"
+ const val SETTINGS = "settings"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/navigation/NavGraph.kt b/app/src/main/java/com/reysand/files/ui/navigation/NavGraph.kt
new file mode 100644
index 0000000..626b226
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/navigation/NavGraph.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import com.reysand.files.ui.screens.FileListScreen
+import com.reysand.files.ui.screens.HomeScreen
+import com.reysand.files.ui.screens.SettingsScreen
+import com.reysand.files.ui.viewmodel.FilesViewModel
+
+/**
+ * Composable function for setting up the navigation graph.
+ *
+ * @param filesViewModel The [FilesViewModel] providing data for the screen.
+ * @param navController NavHostController for managing navigation within the app.
+ * @param modifier Modifier for customizing the layout.
+ */
+@Composable
+fun NavGraph(
+ filesViewModel: FilesViewModel,
+ navController: NavHostController,
+ modifier: Modifier = Modifier
+) {
+ NavHost(navController = navController, startDestination = Destinations.HOME) {
+ composable(Destinations.HOME) {
+ HomeScreen(filesViewModel = filesViewModel, navController = navController, modifier = modifier)
+ }
+ composable(Destinations.FILE_LIST) {
+ FileListScreen(filesViewModel = filesViewModel)
+ }
+ composable(Destinations.SETTINGS) {
+ SettingsScreen()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/screens/FileListScreen.kt b/app/src/main/java/com/reysand/files/ui/screens/FileListScreen.kt
new file mode 100644
index 0000000..0a58260
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/screens/FileListScreen.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.screens
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import com.reysand.files.data.model.FileModel
+import com.reysand.files.ui.components.FileListItem
+import com.reysand.files.ui.components.PermissionAlertDialog
+import com.reysand.files.ui.viewmodel.FilesViewModel
+
+/**
+ * Composable function for displaying a list of files.
+ *
+ * @param filesViewModel The [FilesViewModel] providing data for the screen.
+ * @param modifier Modifier for customizing the layout.
+ */
+@Composable
+fun FileListScreen(
+ filesViewModel: FilesViewModel,
+ modifier: Modifier = Modifier
+) {
+ // Collect the list of files as state
+ val files by filesViewModel.files.collectAsState(initial = emptyList())
+
+ // Display permission alert dialog if needed
+ PermissionAlertDialog(showPermissionDialog = filesViewModel.showPermissionDialog)
+
+ LazyColumn(modifier = modifier) {
+ items(files) { file ->
+ FileListItem(
+ file = file,
+ homeDirectory = filesViewModel.homeDirectory,
+ moveOperation = { fileModel, path -> filesViewModel.moveFile(fileModel, path) },
+ copyOperation = { fileModel, path -> filesViewModel.copyFile(fileModel, path) },
+ renameOperation = { fileModel, path -> filesViewModel.renameFile(fileModel, path) },
+ deleteOperation = { path -> filesViewModel.deleteFile(path) }
+ ) {
+ if (file.fileType == FileModel.FileType.DIRECTORY) {
+ filesViewModel.getFiles(file.path)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/screens/HomeScreen.kt b/app/src/main/java/com/reysand/files/ui/screens/HomeScreen.kt
new file mode 100644
index 0000000..6434c8b
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/screens/HomeScreen.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.screens
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavHostController
+import com.reysand.files.R
+import com.reysand.files.ui.components.StorageCard
+import com.reysand.files.ui.navigation.Destinations
+import com.reysand.files.ui.viewmodel.FilesViewModel
+
+/**
+ * Composable function for the Home screen.
+ *
+ * @param filesViewModel The [FilesViewModel] providing data for the screen.
+ * @param navController NavHostController for managing navigation within the app.
+ * @param modifier Modifier for customizing the layout.
+ */
+@Composable
+fun HomeScreen(
+ filesViewModel: FilesViewModel,
+ navController: NavHostController,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center
+ ) {
+ StorageCard(
+ title = stringResource(id = R.string.internal_storage),
+ leadingIcon = R.drawable.ic_internal_storage,
+ info = filesViewModel.getStorageFreeSpace(),
+ ) {
+ navController.navigate(Destinations.FILE_LIST)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/screens/SettingsScreen.kt b/app/src/main/java/com/reysand/files/ui/screens/SettingsScreen.kt
new file mode 100644
index 0000000..efb556f
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/screens/SettingsScreen.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.screens
+
+import android.content.res.Configuration
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.reysand.files.R
+import com.reysand.files.ui.components.allowPermission
+import com.reysand.files.ui.theme.FilesTheme
+
+/**
+ * Composable function for displaying the settings screen.
+ */
+@Composable
+fun SettingsScreen() {
+ val context = LocalContext.current
+
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ SettingsSection(title = stringResource(id = R.string.settings_privacy_title)) {
+ ListItem(
+ headlineContent = {
+ Text(text = stringResource(id = R.string.file_access_permission_title))
+ },
+ modifier = Modifier.clickable(onClick = { allowPermission(context) }),
+ trailingContent = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_launch),
+ contentDescription = null
+ )
+ }
+ )
+ }
+ }
+}
+
+/**
+ * Composable function for a settings section with a title.
+ *
+ * @param title The title of the settings section.
+ * @param modifier [Modifier] for customizing the layout.
+ * @param content Content of the settings section.
+ */
+@Composable
+fun SettingsSection(
+ title: String,
+ modifier: Modifier = Modifier,
+ content: @Composable (ColumnScope.() -> Unit)
+) {
+ Text(
+ text = title,
+ modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold
+ )
+
+ Card(
+ modifier = modifier.fillMaxWidth()
+ ) {
+ Column(content = content)
+ }
+}
+
+@Preview(name = "Light Mode")
+@Preview(
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+ name = "Dark Mode"
+)
+@Composable
+fun SettingsSectionPreview() {
+ FilesTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ ) {
+ SettingsSection(title = stringResource(id = R.string.settings_privacy_title)) {
+ ListItem(
+ headlineContent = {
+ Text(text = stringResource(id = R.string.file_access_permission_title))
+ },
+ trailingContent = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_launch),
+ contentDescription = null
+ )
+ }
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/util/ContextWrapper.kt b/app/src/main/java/com/reysand/files/ui/util/ContextWrapper.kt
new file mode 100644
index 0000000..5a599a1
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/util/ContextWrapper.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.util
+
+import android.content.Context
+
+/**
+ * Wrapper class for [Context].
+ *
+ * @param context The [Context] to be wrapped.
+ */
+class ContextWrapper(private val context: Context) {
+
+ /**
+ * Returns the wrapped [Context].
+ *
+ * @return The wrapped [Context].
+ */
+ fun getContext(): Context = context
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/reysand/files/ui/viewmodel/FilesViewModel.kt b/app/src/main/java/com/reysand/files/ui/viewmodel/FilesViewModel.kt
new file mode 100644
index 0000000..688e125
--- /dev/null
+++ b/app/src/main/java/com/reysand/files/ui/viewmodel/FilesViewModel.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2023 Andrey Slyusar
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.reysand.files.ui.viewmodel
+
+import android.os.Environment
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
+import com.reysand.files.FilesApplication
+import com.reysand.files.R
+import com.reysand.files.data.model.FileModel
+import com.reysand.files.data.repository.FileRepository
+import com.reysand.files.ui.util.ContextWrapper
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import java.io.File
+
+/**
+ * ViewModel for managing file-related data and operations.
+ *
+ * @param fileRepository The repository for accessing file data.
+ * @param contextWrapper The wrapper for accessing the application context.
+ */
+class FilesViewModel(
+ private val fileRepository: FileRepository,
+ private val contextWrapper: ContextWrapper
+) : ViewModel() {
+
+ // MutableStateFlow holding the list of files
+ private var _files = MutableStateFlow>(emptyList())
+ val files = _files.asStateFlow()
+
+ // Paths for the home and current directories
+ val homeDirectory = Environment.getExternalStorageDirectory().path
+ val currentDirectory = mutableStateOf(homeDirectory)
+
+ // State indicating whether to show the permission dialog
+ val showPermissionDialog = mutableStateOf(!Environment.isExternalStorageManager())
+
+ // Initialize the ViewModel by loading files from the home directory
+ init {
+ getFiles(homeDirectory)
+ }
+
+ /**
+ * Get a list of files from the specified path.
+ *
+ * @param path The path to the directory containing the files.
+ */
+ fun getFiles(path: String) {
+ viewModelScope.launch {
+ _files.value = fileRepository.getFiles(path)
+ currentDirectory.value = path
+ }
+ }
+
+ /**
+ * Navigate up to the parent directory,
+ */
+ fun navigateUp() {
+ val parentDirectory = File(currentDirectory.value).parent
+
+ if (currentDirectory.value != homeDirectory && parentDirectory != null) {
+ getFiles(parentDirectory)
+ }
+ }
+
+ /**
+ * Gets the free space of the storage.
+ *
+ * @return A string representing the free space of the storage.
+ */
+ fun getStorageFreeSpace(): String {
+ return contextWrapper.getContext().getString(
+ R.string.storage_free_space,
+ fileRepository.getStorageFreeSpace()
+ )
+ }
+
+ /**
+ * Move a file from one path to another.
+ *
+ * @param file The file to move.
+ * @param destination The destination path.
+ */
+ fun moveFile(file: FileModel, destination: String) {
+ viewModelScope.launch {
+ if (fileRepository.moveFile(file.path, homeDirectory.plus(destination))) {
+ getFiles(currentDirectory.value)
+ }
+ }
+ }
+
+ /**
+ * Copy a file from one path to another.
+ *
+ * @param file The file to copy.
+ * @param destination The destination path.
+ */
+ fun copyFile(file: FileModel, destination: String) {
+ viewModelScope.launch {
+ if (fileRepository.copyFile(file.path, homeDirectory.plus(destination))) {
+ getFiles(currentDirectory.value)
+ }
+ }
+ }
+
+ /**
+ * Rename a file.
+ *
+ * @param file The file to rename.
+ * @param newName The new name of the file.
+ */
+ fun renameFile(file: FileModel, newName: String) {
+ viewModelScope.launch {
+ if (fileRepository.renameFile(
+ file.path,
+ file.path.removeSuffix(file.name).plus(newName)
+ )
+ ) {
+ getFiles(currentDirectory.value)
+ }
+ }
+ }
+
+ /**
+ * Delete a file.
+ *
+ * @param path The path of the file to delete.
+ */
+ fun deleteFile(path: String) {
+ viewModelScope.launch {
+ if (fileRepository.deleteFile(path)) {
+ getFiles(currentDirectory.value)
+ }
+ }
+ }
+
+ companion object {
+ // Factory for creating FilesViewModel instances
+ val Factory: ViewModelProvider.Factory = viewModelFactory {
+ initializer {
+ val application = (this[APPLICATION_KEY] as FilesApplication)
+ val fileRepository = application.container.fileRepository
+ val contextWrapper = ContextWrapper(application.applicationContext)
+ FilesViewModel(fileRepository = fileRepository, contextWrapper = contextWrapper)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_file.xml b/app/src/main/res/drawable/ic_file.xml
new file mode 100644
index 0000000..d33224a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_file.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_folder.xml b/app/src/main/res/drawable/ic_folder.xml
new file mode 100644
index 0000000..2c8a4ee
--- /dev/null
+++ b/app/src/main/res/drawable/ic_folder.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_internal_storage.xml b/app/src/main/res/drawable/ic_internal_storage.xml
new file mode 100644
index 0000000..583a811
--- /dev/null
+++ b/app/src/main/res/drawable/ic_internal_storage.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launch.xml b/app/src/main/res/drawable/ic_launch.xml
new file mode 100644
index 0000000..22d042a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launch.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_option_copy.xml b/app/src/main/res/drawable/ic_option_copy.xml
new file mode 100644
index 0000000..8af2d28
--- /dev/null
+++ b/app/src/main/res/drawable/ic_option_copy.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_option_delete.xml b/app/src/main/res/drawable/ic_option_delete.xml
new file mode 100644
index 0000000..841abc9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_option_delete.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_option_move.xml b/app/src/main/res/drawable/ic_option_move.xml
new file mode 100644
index 0000000..f4f2d81
--- /dev/null
+++ b/app/src/main/res/drawable/ic_option_move.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_option_rename.xml b/app/src/main/res/drawable/ic_option_rename.xml
new file mode 100644
index 0000000..b669810
--- /dev/null
+++ b/app/src/main/res/drawable/ic_option_rename.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 49f82b2..970c671 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -15,4 +15,29 @@
-->
Files
+
+
+ OK
+ Cancel
+ Delete
+ Are you sure you want to delete \'%1$s\'?
+
+
+ File Access Permission
+ This app requires access to manage all files on your device for proper functionality. Please grant the permission.
+ Grant Permission
+
+
+ Move to
+ Copy to
+ Rename
+ Delete
+
+
+ Internal Storage
+ %1$s free
+
+
+ Settings
+ Privacy
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 8b82b3f..87a11f3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id 'com.android.application' version '8.1.1' apply false
- id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
+ alias libs.plugins.android.application apply false
+ alias libs.plugins.kotlin.android apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..2574ce2
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,34 @@
+[versions]
+activity-compose = "1.7.2"
+agp = "8.1.3"
+androidx-test-ext-junit = "1.1.5"
+compose-bom = "2023.06.01"
+core-ktx = "1.10.1"
+espresso-core = "3.5.1"
+junit = "4.13.2"
+kotlin = "1.9.20"
+lifecycle-runtime-ktx = "2.6.2"
+navigation-compose = "2.6.0"
+
+[libraries]
+activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
+androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
+compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
+core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
+lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle-runtime-ktx" }
+material3 = { group = "androidx.compose.material3", name = "material3" }
+navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" }
+ui = { group = "androidx.compose.ui", name = "ui" }
+ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7fdf476..48b7bb1 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Sep 06 17:00:14 MSK 2023
+#Fri Nov 10 19:07:23 MSK 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists