From 8cc2df989b9ed5d67f4def48bafc9dd3511fd5e2 Mon Sep 17 00:00:00 2001 From: Thomas Bernard Date: Sat, 21 Sep 2024 15:20:08 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=81=20Add=20file=20explorer=20feature?= =?UTF-8?q?=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Can delete a file * Fix bug, can't delete file with spaces * Add some pictures for files * Can use finder to upload file * Can open save dialog * Add logging + can download a file * Clean up --- composeApp/build.gradle.kts | 2 + .../composeResources/drawable/android.xml | 5 + .../composeResources/drawable/file.xml | 14 ++- .../composeResources/drawable/image.xml | 9 ++ .../composeResources/drawable/video.xml | 9 ++ .../commons/di/AndroidToolsModule.kt | 6 + .../data/datasources/ShellDataSource.kt | 15 ++- .../data/repositories/DeviceRepositoryImpl.kt | 3 - .../data/repositories/FileRepositoryImpl.kt | 8 ++ .../domain/repositories/FileRepository.kt | 17 +++ .../device/GetDeviceInformationUseCase.kt | 4 - .../domain/usecases/file/DeleteFileUseCase.kt | 12 ++ .../usecases/file/DownloadFileUseCase.kt | 12 ++ .../fr/thomasbernard03/androidtools/main.kt | 3 + .../commons/extensions/StringExtensions.kt | 62 ++++++++++ .../fileexplorer/FileExplorerEvent.kt | 5 + .../fileexplorer/FileExplorerScreen.kt | 106 ++++++++++++++++-- .../fileexplorer/FileExplorerUiState.kt | 2 + .../fileexplorer/FileExplorerViewModel.kt | 45 ++++++-- .../components/FileExplorerItem.kt | 76 +++++++------ .../fileexplorer/components/FileItem.kt | 33 ++++-- .../fileexplorer/components/FolderItem.kt | 32 +++++- .../information/InformationViewModel.kt | 2 +- .../androidtools/presentation/theme/Color.kt | 7 +- 24 files changed, 405 insertions(+), 84 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/drawable/android.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/image.xml create mode 100644 composeApp/src/commonMain/composeResources/drawable/video.xml create mode 100644 composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/file/DeleteFileUseCase.kt create mode 100644 composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/file/DownloadFileUseCase.kt create mode 100644 composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/commons/extensions/StringExtensions.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 1882656..9a02e56 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -35,6 +35,8 @@ kotlin { implementation(libs.koin.core) implementation(libs.kotlinx.datetime) + + implementation("io.klogging:klogging-jvm:0.7.2") } // https://gist.github.com/OysterD3?page=3 // https://betterprogramming.pub/how-to-create-an-auto-updater-for-desktop-application-jetpack-compose-d118db26d65f diff --git a/composeApp/src/commonMain/composeResources/drawable/android.xml b/composeApp/src/commonMain/composeResources/drawable/android.xml new file mode 100644 index 0000000..8b89a3d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/android.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/file.xml b/composeApp/src/commonMain/composeResources/drawable/file.xml index d3e7b21..dfd8c99 100644 --- a/composeApp/src/commonMain/composeResources/drawable/file.xml +++ b/composeApp/src/commonMain/composeResources/drawable/file.xml @@ -1,3 +1,13 @@ - - + + diff --git a/composeApp/src/commonMain/composeResources/drawable/image.xml b/composeApp/src/commonMain/composeResources/drawable/image.xml new file mode 100644 index 0000000..59b1d4f --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/image.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/video.xml b/composeApp/src/commonMain/composeResources/drawable/video.xml new file mode 100644 index 0000000..ec3a20f --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/video.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/commons/di/AndroidToolsModule.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/commons/di/AndroidToolsModule.kt index 3594346..b859975 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/commons/di/AndroidToolsModule.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/commons/di/AndroidToolsModule.kt @@ -13,9 +13,15 @@ import fr.thomasbernard03.androidtools.domain.repositories.ApplicationRepository import fr.thomasbernard03.androidtools.domain.repositories.DeviceRepository import fr.thomasbernard03.androidtools.domain.repositories.FileRepository import fr.thomasbernard03.androidtools.domain.repositories.LogcatRepository +import io.klogging.Klogger +import io.klogging.logger import org.koin.dsl.module val androidToolsModule = module { + // Logger + single { logger("main") } + + // Datasource single { ShellDataSource() } diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/datasources/ShellDataSource.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/datasources/ShellDataSource.kt index f435cc5..8473d2e 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/datasources/ShellDataSource.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/datasources/ShellDataSource.kt @@ -1,27 +1,24 @@ package fr.thomasbernard03.androidtools.data.datasources -import androidtools.composeapp.generated.resources.Res import com.russhwolf.settings.Settings import fr.thomasbernard03.androidtools.commons.SettingsConstants import fr.thomasbernard03.androidtools.commons.helpers.AdbProviderHelper +import io.klogging.Klogger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.withContext -import org.jetbrains.compose.resources.ExperimentalResourceApi import org.koin.java.KoinJavaComponent.get import java.io.BufferedReader -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream import java.io.InputStreamReader -import java.util.zip.ZipInputStream class ShellDataSource( private val settings: Settings = Settings(), + private val logger : Klogger = get(Klogger::class.java), private val adbProviderHelper: AdbProviderHelper = get(AdbProviderHelper::class.java) ) { suspend fun executeAdbCommand(vararg formatArgs: String): String = withContext(Dispatchers.IO) { + logger.info("*** Start executing adb command ***") val arguments = mutableListOf() val currentDevice = settings.getStringOrNull(key = SettingsConstants.SELECTED_DEVICE_KEY) @@ -34,6 +31,8 @@ class ShellDataSource( arguments.addAll(formatArgs) val adb = adbProviderHelper.getAdb() + logger.debug("Adb path: ${adb.absolutePath}") + logger.info("${adb.absolutePath} ${arguments.joinToString(" ")}") val process = ProcessBuilder(listOf(adb.absolutePath) + arguments).start() val reader = BufferedReader(InputStreamReader(process.inputStream)) @@ -45,6 +44,10 @@ class ShellDataSource( process.waitFor() + logger.info("$output") + + logger.info("*** End of adb command ***") + return@withContext output.toString() } diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/repositories/DeviceRepositoryImpl.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/repositories/DeviceRepositoryImpl.kt index 0a01f7a..9dcaf6b 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/repositories/DeviceRepositoryImpl.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/repositories/DeviceRepositoryImpl.kt @@ -1,6 +1,5 @@ package fr.thomasbernard03.androidtools.data.repositories -import fr.thomasbernard03.androidtools.commons.SettingsConstants import fr.thomasbernard03.androidtools.data.datasources.ShellDataSource import fr.thomasbernard03.androidtools.domain.models.DeviceInformation import fr.thomasbernard03.androidtools.domain.repositories.DeviceRepository @@ -11,8 +10,6 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext import org.koin.java.KoinJavaComponent.get -import java.io.BufferedReader -import java.io.InputStreamReader class DeviceRepositoryImpl( private val shellDataSource: ShellDataSource = get(ShellDataSource::class.java) diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/repositories/FileRepositoryImpl.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/repositories/FileRepositoryImpl.kt index 0d5531a..e9c0fb4 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/repositories/FileRepositoryImpl.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/data/repositories/FileRepositoryImpl.kt @@ -10,4 +10,12 @@ class FileRepositoryImpl( override suspend fun uploadFile(path: String, targetPath : String) { shellDataSource.executeAdbCommand("push", path, targetPath) } + + override suspend fun deleteFile(path: String) { + shellDataSource.executeAdbCommand("shell", "rm", "'$path'") + } + + override suspend fun downloadFile(path: String, targetPath: String) { + shellDataSource.executeAdbCommand("pull", path, targetPath) + } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/repositories/FileRepository.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/repositories/FileRepository.kt index 98d5a0d..c981a04 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/repositories/FileRepository.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/repositories/FileRepository.kt @@ -1,5 +1,22 @@ package fr.thomasbernard03.androidtools.domain.repositories interface FileRepository { + /** + * Upload a file from the computer to the android device + * @param path the path of the file to upload (On the computer) + * @param targetPath the path where to upload the file (On the android device) + */ suspend fun uploadFile(path: String, targetPath : String) + + + /** + * Download a file from the android device to the computer + * @param path the path of the file to download (On the android device) + * @param targetPath the path where to download the file (On the computer) + */ + suspend fun downloadFile(path: String, targetPath : String) + + + + suspend fun deleteFile(path: String) } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/device/GetDeviceInformationUseCase.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/device/GetDeviceInformationUseCase.kt index bc5b703..d50c169 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/device/GetDeviceInformationUseCase.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/device/GetDeviceInformationUseCase.kt @@ -1,14 +1,10 @@ package fr.thomasbernard03.androidtools.domain.usecases.device -import com.russhwolf.settings.Settings -import fr.thomasbernard03.androidtools.commons.SettingsConstants import fr.thomasbernard03.androidtools.domain.models.DeviceInformation import fr.thomasbernard03.androidtools.domain.repositories.DeviceRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.java.KoinJavaComponent.get -import java.io.BufferedReader -import java.io.InputStreamReader class GetDeviceInformationUseCase( private val deviceRepository: DeviceRepository = get(DeviceRepository::class.java) diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/file/DeleteFileUseCase.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/file/DeleteFileUseCase.kt new file mode 100644 index 0000000..568c286 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/file/DeleteFileUseCase.kt @@ -0,0 +1,12 @@ +package fr.thomasbernard03.androidtools.domain.usecases.file + +import fr.thomasbernard03.androidtools.domain.repositories.FileRepository +import org.koin.java.KoinJavaComponent.get + +class DeleteFileUseCase( + private val fileRepository: FileRepository = get(FileRepository::class.java) +) { + suspend operator fun invoke(path: String) { + fileRepository.deleteFile(path) + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/file/DownloadFileUseCase.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/file/DownloadFileUseCase.kt new file mode 100644 index 0000000..d9a7903 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/domain/usecases/file/DownloadFileUseCase.kt @@ -0,0 +1,12 @@ +package fr.thomasbernard03.androidtools.domain.usecases.file + +import fr.thomasbernard03.androidtools.domain.repositories.FileRepository +import org.koin.java.KoinJavaComponent.get + +class DownloadFileUseCase( + private val fileRepository: FileRepository = get(FileRepository::class.java) +) { + suspend operator fun invoke(path: String, targetPath: String) { + fileRepository.downloadFile(path, targetPath) + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/main.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/main.kt index f8ecc0a..65a0654 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/main.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/main.kt @@ -11,11 +11,14 @@ import fr.thomasbernard03.androidtools.commons.di.androidToolsModule import fr.thomasbernard03.androidtools.presentation.main.MainScreen import fr.thomasbernard03.androidtools.presentation.main.MainViewModel import fr.thomasbernard03.androidtools.presentation.theme.AndroidToolsTheme +import io.klogging.config.DEFAULT_CONSOLE +import io.klogging.config.loggingConfiguration import org.jetbrains.compose.resources.stringResource import org.koin.core.context.GlobalContext.startKoin fun main() = application { startKoin { modules(androidToolsModule) } + loggingConfiguration { DEFAULT_CONSOLE() } Window( onCloseRequest = ::exitApplication, diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/commons/extensions/StringExtensions.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/commons/extensions/StringExtensions.kt new file mode 100644 index 0000000..9b177c0 --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/commons/extensions/StringExtensions.kt @@ -0,0 +1,62 @@ +package fr.thomasbernard03.androidtools.presentation.commons.extensions + +import androidtools.composeapp.generated.resources.Res +import androidtools.composeapp.generated.resources.android +import androidtools.composeapp.generated.resources.file +import androidtools.composeapp.generated.resources.image +import androidtools.composeapp.generated.resources.video +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import fr.thomasbernard03.androidtools.presentation.theme.AndroidColor +import fr.thomasbernard03.androidtools.presentation.theme.FileColor +import fr.thomasbernard03.androidtools.presentation.theme.ImageColor +import fr.thomasbernard03.androidtools.presentation.theme.VideoColor +import org.jetbrains.compose.resources.painterResource + +@Composable +fun String.fileIcon() { + if (this.endsWith(".apk")) { + Icon( + painter = painterResource(Res.drawable.android), + contentDescription = "Android icon", + tint = AndroidColor, + modifier = Modifier.size(24.dp) + ) + } + else if (this.endsWith(".mp4") || this.endsWith(".avi") || this.endsWith(".mkv")) { + Icon( + painter = painterResource(Res.drawable.video), + contentDescription = "Video icon", + modifier = Modifier.size(24.dp), + tint = VideoColor + ) + } +// else if (this.endsWith(".mp3") || this.endsWith(".wav") || this.endsWith(".flac")) { +// Icon( +// painter = painterResource(Res.drawable.music), +// contentDescription = "", +// modifier = Modifier.size(24.dp), +// tint = MaterialTheme.colorScheme.onBackground +// ) +// } + else if (this.endsWith(".jpg", ignoreCase = true) || this.endsWith(".jpeg") || this.endsWith(".png", ignoreCase = true) || this.endsWith(".gif")) { + Icon( + painter = painterResource(Res.drawable.image), + contentDescription = "", + modifier = Modifier.size(24.dp).padding(4.dp), + tint = ImageColor + ) + } + else { + Icon( + painter = painterResource(Res.drawable.file), + contentDescription = "", + modifier = Modifier.size(24.dp), + tint = FileColor + ) + } +} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerEvent.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerEvent.kt index 4728712..13bd214 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerEvent.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerEvent.kt @@ -1,5 +1,6 @@ package fr.thomasbernard03.androidtools.presentation.fileexplorer +import fr.thomasbernard03.androidtools.domain.models.File import fr.thomasbernard03.androidtools.domain.models.Folder import fr.thomasbernard03.androidtools.presentation.commons.Event @@ -12,4 +13,8 @@ sealed class FileExplorerEvent : Event { data object OnRefresh : FileExplorerEvent() data class OnAddFile(val path : String) : FileExplorerEvent() + data class OnDelete(val path : String) : FileExplorerEvent() + data class OnDownload(val path : String, val targetPath : String) : FileExplorerEvent() + + data class OnFileSelected(val file : File?) : FileExplorerEvent() } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerScreen.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerScreen.kt index e759d21..11d4243 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerScreen.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerScreen.kt @@ -1,10 +1,15 @@ package fr.thomasbernard03.androidtools.presentation.fileexplorer import androidtools.composeapp.generated.resources.Res +import androidtools.composeapp.generated.resources.app_installer import androidtools.composeapp.generated.resources.arrow_back +import androidtools.composeapp.generated.resources.folder +import androidtools.composeapp.generated.resources.open_file_explorer import androidtools.composeapp.generated.resources.replay +import androidtools.composeapp.generated.resources.trash import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,15 +19,15 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn +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.lazy.grid.rememberLazyGridState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator @@ -40,14 +45,18 @@ import androidx.compose.ui.DragData import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.onExternalDrag import androidx.compose.ui.unit.dp import fr.thomasbernard03.androidtools.commons.extensions.getParents import fr.thomasbernard03.androidtools.domain.models.Folder -import fr.thomasbernard03.androidtools.presentation.applicationinstaller.ApplicationInstallerEvent import fr.thomasbernard03.androidtools.presentation.fileexplorer.components.FileItem import fr.thomasbernard03.androidtools.presentation.fileexplorer.components.FolderItem +import fr.thomasbernard03.androidtools.presentation.theme.FolderColor import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import java.awt.FileDialog +import java.awt.Frame import java.net.URLDecoder @OptIn(ExperimentalComposeUiApi::class) @@ -82,7 +91,25 @@ fun FileExplorerScreen( }, ), floatingActionButton = { + FloatingActionButton( + onClick = { + val fileDialog = FileDialog(Frame(), "", FileDialog.LOAD) + fileDialog.isVisible = true + val selectedFile = fileDialog.file + val directory = fileDialog.directory + selectedFile?.let { + val filePath = "$directory$selectedFile" + onEvent(FileExplorerEvent.OnAddFile(filePath)) + } + } + ){ + Icon( + painter = painterResource(Res.drawable.folder), + contentDescription = stringResource(Res.string.open_file_explorer), + tint = MaterialTheme.colorScheme.onBackground + ) + } } ) { Box( @@ -115,12 +142,34 @@ fun FileExplorerScreen( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(MaterialTheme.colorScheme.background) - .weight(1f), + .weight(1f) + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text( - text = uiState.folder?.getParents()?.joinToString(" -> ") { it.name } ?: "", - modifier = Modifier.padding(8.dp), - ) + uiState.folder?.getParents()?.forEach { parent -> + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(Res.drawable.folder), + contentDescription = parent.name, + tint = FolderColor + ) + Text( + text = parent.name, + modifier = Modifier.padding(8.dp), + ) + + Icon( + painter = painterResource(Res.drawable.arrow_back), + contentDescription = parent.name, + tint = MaterialTheme.colorScheme.onBackground.copy(0.7f), + modifier = Modifier.rotate(180f).size(12.dp) + ) + } + } } Row { @@ -132,13 +181,42 @@ fun FileExplorerScreen( contentDescription = "Refresh", ) } + + IconButton( + enabled = uiState.selectedFile != null, + onClick = { uiState.selectedFile?.let { onEvent(FileExplorerEvent.OnDelete("${it.path}/${it.name}")) }} + ) { + Icon( + painter = painterResource(Res.drawable.trash), + contentDescription = "Delete", + ) + } + + IconButton( + enabled = uiState.selectedFile != null, + onClick = { + uiState.selectedFile?.let { fileToDownload -> + // Open file dialog + val fileDialog = FileDialog(Frame(), "", FileDialog.SAVE) + fileDialog.isVisible = true + fileDialog.isModal = true + val directory = fileDialog.directory + onEvent(FileExplorerEvent.OnDownload("${fileToDownload.path}/${fileToDownload.name}", directory)) + } + } + ) { + Icon( + painter = painterResource(Res.drawable.app_installer), + contentDescription = "Download", + ) + } } } Box { LazyVerticalGrid( state = state, - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(8.dp), columns = GridCells.Adaptive(300.dp), @@ -160,6 +238,14 @@ fun FileExplorerScreen( name = file.name, size = file.size, modifiedAt = file.modifiedAt, + selected = uiState.selectedFile?.name == file.name, + onClick = { + if (uiState.selectedFile?.name == file.name) { + onEvent(FileExplorerEvent.OnFileSelected(null)) + } else { + onEvent(FileExplorerEvent.OnFileSelected(file)) + } + } ) } } diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerUiState.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerUiState.kt index d546995..9407da8 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerUiState.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerUiState.kt @@ -7,4 +7,6 @@ import fr.thomasbernard03.androidtools.presentation.commons.UiState data class FileExplorerUiState( val loading : Boolean = false, val folder: Folder? = null, + + val selectedFile : File? = null ) : UiState \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerViewModel.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerViewModel.kt index 744b989..4ac31b2 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerViewModel.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/FileExplorerViewModel.kt @@ -3,34 +3,39 @@ package fr.thomasbernard03.androidtools.presentation.fileexplorer import androidx.lifecycle.viewModelScope import fr.thomasbernard03.androidtools.domain.models.Folder import fr.thomasbernard03.androidtools.domain.usecases.GetFilesUseCase +import fr.thomasbernard03.androidtools.domain.usecases.file.DeleteFileUseCase +import fr.thomasbernard03.androidtools.domain.usecases.file.DownloadFileUseCase import fr.thomasbernard03.androidtools.domain.usecases.file.UploadFileUseCase import fr.thomasbernard03.androidtools.presentation.commons.BaseViewModel import kotlinx.coroutines.launch class FileExplorerViewModel( private val getFilesUseCase: GetFilesUseCase = GetFilesUseCase(), - private val uploadFileUseCase: UploadFileUseCase = UploadFileUseCase() + private val uploadFileUseCase: UploadFileUseCase = UploadFileUseCase(), + private val deleteFileUseCase : DeleteFileUseCase = DeleteFileUseCase(), + private val downloadFileUseCase: DownloadFileUseCase = DownloadFileUseCase() ) : BaseViewModel() { override fun initializeUiState() = FileExplorerUiState() override fun onEvent(event: FileExplorerEvent) { when(event){ + is FileExplorerEvent.OnFileSelected -> updateUiState { copy(selectedFile = event.file) } FileExplorerEvent.OnAppearing -> { viewModelScope.launch { updateUiState { copy(loading = true) } - val files = getFilesUseCase(path = "") + val files = getFilesUseCase(path = "storage/emulated/0") val folder = Folder().apply { childens = files - name = "/" - path = "" + name = "" + path = "storage/emulated/0" } updateUiState { copy(loading = false, folder = folder) } } } is FileExplorerEvent.OnGetFiles -> { viewModelScope.launch { - updateUiState { copy(loading = true) } + updateUiState { copy(loading = true, selectedFile = null) } val files = getFilesUseCase(path = "${event.folder.path}/${event.folder.name}") val newFolder = event.folder.apply { @@ -41,17 +46,16 @@ class FileExplorerViewModel( updateUiState { copy(loading = false, folder = newFolder) } } } - FileExplorerEvent.OnGoBack -> { uiState.value.folder?.parent?.let { parent -> - updateUiState { copy(loading = false, folder = parent) } + updateUiState { copy(folder = parent, selectedFile = null) } } } FileExplorerEvent.OnRefresh -> { uiState.value.folder?.let { folder -> viewModelScope.launch { - updateUiState { copy(loading = true) } + updateUiState { copy(loading = true, selectedFile = null) } val files = getFilesUseCase(path = "${folder.path}/${folder.name}") folder.childens = files @@ -64,7 +68,7 @@ class FileExplorerViewModel( is FileExplorerEvent.OnAddFile -> { uiState.value.folder?.let { targetFolder -> viewModelScope.launch { - updateUiState { copy(loading = true) } + updateUiState { copy(loading = true, selectedFile = null) } uploadFileUseCase(event.path, "${targetFolder.path}/${targetFolder.name}") val files = getFilesUseCase(path = "${targetFolder.path}/${targetFolder.name}") targetFolder.childens = files @@ -72,6 +76,29 @@ class FileExplorerViewModel( } } } + is FileExplorerEvent.OnDelete -> { + viewModelScope.launch { + uiState.value.folder?.let { folder -> + updateUiState { copy(loading = true, selectedFile = null) } + deleteFileUseCase(event.path) + val files = getFilesUseCase(path = "${folder.path}/${folder.name}") + folder.childens = files + updateUiState { copy(loading = false, folder = folder) } + } + } + } + + is FileExplorerEvent.OnDownload -> { + viewModelScope.launch { + uiState.value.folder?.let { folder -> + updateUiState { copy(loading = true, selectedFile = null) } + downloadFileUseCase(event.path, event.targetPath) + val files = getFilesUseCase(path = "${uiState.value.folder?.path}/${uiState.value.folder?.name}") + folder.childens = files + updateUiState { copy(loading = false, folder = folder) } + } + } + } } } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FileExplorerItem.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FileExplorerItem.kt index 2ebf624..a035150 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FileExplorerItem.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FileExplorerItem.kt @@ -1,28 +1,23 @@ package fr.thomasbernard03.androidtools.presentation.fileexplorer.components +import androidx.compose.foundation.BorderStroke 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.fillMaxWidth -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton 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.Color -import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import fr.thomasbernard03.androidtools.commons.extensions.byteCountToDisplaySize -import fr.thomasbernard03.androidtools.presentation.theme.FolderColor -import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.painterResource -import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -30,46 +25,50 @@ import java.time.format.DateTimeFormatter fun FileExplorerItem( modifier: Modifier = Modifier, onClick : () -> Unit, - icon : DrawableResource, - iconTint : Color = MaterialTheme.colorScheme.onBackground, + onDoubleClick : () -> Unit = {}, + leadingIcon : @Composable () -> Unit, + subTitle : @Composable () -> Unit = {}, name: String, - size : Long, - modifiedAt : LocalDateTime + modifiedAt : LocalDateTime, + colors : ButtonColors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onBackground, + ) ) { - Button( - modifier = modifier, + OutlinedButton( + modifier = modifier, onClick = onClick, shape = RoundedCornerShape(4.dp), contentPadding = PaddingValues(16.dp), elevation = ButtonDefaults.buttonElevation( - defaultElevation = 4.dp, - pressedElevation = 4.dp, - hoveredElevation = 4.dp, - focusedElevation = 4.dp + defaultElevation = 0.dp, + pressedElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp ), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onBackground, - ) + colors = colors, + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f)), ){ Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth(), ) { - Icon( - painter = painterResource(icon), - contentDescription = name, - modifier = Modifier.size(24.dp), - tint = iconTint - ) + leadingIcon() - Text( + Column( modifier = Modifier.weight(1f), - textAlign = TextAlign.Start, - text = name, - style = MaterialTheme.typography.bodySmall - ) + ) { + Text( + textAlign = TextAlign.Start, + text = name.substringBeforeLast("."), + style = MaterialTheme.typography.bodySmall, + maxLines = 1 + ) + subTitle() + } + + // Text( // text = size.byteCountToDisplaySize(), @@ -80,7 +79,10 @@ fun FileExplorerItem( val outputFormatter = DateTimeFormatter.ofPattern("HH:mm MMM dd, yyyy") Text( text = modifiedAt.format(outputFormatter), - style = MaterialTheme.typography.bodySmall + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f) + ), + fontWeight = FontWeight.SemiBold ) } } diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FileItem.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FileItem.kt index f97d77c..cfaec2d 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FileItem.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FileItem.kt @@ -1,9 +1,13 @@ package fr.thomasbernard03.androidtools.presentation.fileexplorer.components -import androidtools.composeapp.generated.resources.Res -import androidtools.composeapp.generated.resources.file +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import fr.thomasbernard03.androidtools.commons.extensions.byteCountToDisplaySize +import fr.thomasbernard03.androidtools.presentation.commons.extensions.fileIcon import java.time.LocalDateTime @Composable @@ -11,14 +15,29 @@ fun FileItem( modifier: Modifier = Modifier, name: String, size : Long, - modifiedAt : LocalDateTime + modifiedAt : LocalDateTime, + selected : Boolean, + onClick : () -> Unit ) { FileExplorerItem( modifier = modifier, - onClick = {}, - icon = Res.drawable.file, + onClick = onClick, name = name, - size = size, - modifiedAt = modifiedAt + modifiedAt = modifiedAt, + colors = ButtonDefaults.buttonColors( + containerColor = if (!selected) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onBackground, + ), + leadingIcon = { name.fileIcon() }, + subTitle = { + Text( + textAlign = TextAlign.Start, + text = name.substringAfterLast(".") + " / " + size.byteCountToDisplaySize(), + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f) + ), + maxLines = 1 + ) + } ) } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FolderItem.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FolderItem.kt index 7933ca0..5b40f06 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FolderItem.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/fileexplorer/components/FolderItem.kt @@ -2,9 +2,17 @@ package fr.thomasbernard03.androidtools.presentation.fileexplorer.components import androidtools.composeapp.generated.resources.Res import androidtools.composeapp.generated.resources.folder +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import fr.thomasbernard03.androidtools.commons.extensions.byteCountToDisplaySize import fr.thomasbernard03.androidtools.presentation.theme.FolderColor +import org.jetbrains.compose.resources.painterResource import java.time.LocalDateTime @Composable @@ -17,11 +25,27 @@ fun FolderItem( ) { FileExplorerItem( modifier = modifier, + onDoubleClick = onExpand, onClick = onExpand, - icon = Res.drawable.folder, - iconTint = FolderColor, name = name, - size = size, - modifiedAt = modifiedAt + modifiedAt = modifiedAt, + leadingIcon = { + Icon( + painter = painterResource(Res.drawable.folder), + contentDescription = name, + modifier = Modifier.size(24.dp), + tint = FolderColor + ) + }, + subTitle = { + Text( + textAlign = TextAlign.Start, + text = size.byteCountToDisplaySize(), + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f) + ), + maxLines = 1 + ) + } ) } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/information/InformationViewModel.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/information/InformationViewModel.kt index 676152c..0877240 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/information/InformationViewModel.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/information/InformationViewModel.kt @@ -1,8 +1,8 @@ package fr.thomasbernard03.androidtools.presentation.information import androidx.lifecycle.viewModelScope -import fr.thomasbernard03.androidtools.domain.usecases.device.GetDeviceInformationUseCase import fr.thomasbernard03.androidtools.domain.usecases.device.GetDeviceBatteryUseCase +import fr.thomasbernard03.androidtools.domain.usecases.device.GetDeviceInformationUseCase import fr.thomasbernard03.androidtools.presentation.commons.BaseViewModel import kotlinx.coroutines.launch diff --git a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/theme/Color.kt b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/theme/Color.kt index 7edab5f..4d17ad3 100644 --- a/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/theme/Color.kt +++ b/composeApp/src/desktopMain/kotlin/fr/thomasbernard03/androidtools/presentation/theme/Color.kt @@ -15,4 +15,9 @@ val ErrorBackgroundColorDark = Color(0xFFCF5B56) val VerboseBackgroundColorDark = Color(0xFFE0E0E0) -val FolderColor = Color(0xFFEBC86A) \ No newline at end of file +val FolderColor = Color(0xFFEBC86A) + +val AndroidColor = Color(0xFF3CB04D) +val VideoColor = Color(0xFFF88336) +val ImageColor = Color(0xFF9853DC) +val FileColor = Color(0xFFFB4041)