From 93e8f87fd98744e0fbbb46e7f358805f501ac879 Mon Sep 17 00:00:00 2001 From: Juan Nascimento Date: Sun, 18 Feb 2024 12:26:14 -0300 Subject: [PATCH] feat: added support for AI chat and fixed error when clicking to view file properties. --- app/build.gradle.kts | 33 +- app/src/main/AndroidManifest.xml | 17 +- .../compose/core/extensions/MessageExt.kt | 26 + .../filemanager/compose/core/models/Chat.kt | 14 + .../compose/core/models/ChatSettings.kt | 15 + .../compose/core/models/FileOperationItem.kt | 13 + .../core/models/FileOperationResult.kt | 18 + .../compose/core/models/Message.kt | 50 ++ .../navigation/CategoryGraphNavigation.kt | 43 ++ .../core/navigation/CategoryListNavigation.kt | 39 ++ .../compose/core/navigation/ChatNavigation.kt | 43 ++ .../core/navigation/MediaViewNavigation.kt | 44 ++ .../compose/core/navigation/NavigationComp.kt | 44 ++ .../components/LoadingAnimation.kt | 83 +++ .../presentation/components/NavigationComp.kt | 2 - .../core/presentation/util/MessageUtils.kt | 39 ++ .../core/presentation/util/OperationsUtils.kt | 118 +++++ .../compose/core/presentation/util/Prompt.kt | 95 ++++ .../feature/presentation/HomeScreen.kt | 63 +++ .../categorylist/CategoryListScreen.kt | 75 --- .../categorylist/apklist/ApkListScreen.kt | 163 +++--- .../components/CategoryListScreen.kt | 2 +- .../presentation/chat_screen/ChatScreen.kt | 108 ++++ .../presentation/chat_screen/ChatUiState.kt | 15 + .../presentation/chat_screen/ChatViewModel.kt | 133 +++++ .../components/ChatNavigationDrawer.kt | 120 +++++ .../chat_screen/components/ChatTextField.kt | 121 +++++ .../chat_screen/components/CustomTopAppBar.kt | 63 +++ .../components/ListFileOperations.kt | 104 ++++ .../components/ListOperationResults.kt | 77 +++ .../components/MessageDropdownMenu.kt | 44 ++ .../chat_screen/components/MessageItem.kt | 168 ++++++ .../chat_screen/components/MessageList.kt | 41 ++ .../DeletedFileDetailsViewModel.kt | 57 -- .../deletedfiles/DeletedFileEntryViewModel.kt | 112 ---- .../deletedfiles/DeletedIFilesViewModel.kt | 36 -- .../deletedfileslist/DeletedFileListScreen.kt | 418 --------------- .../components/BottomSheetInfo.kt | 173 ------ .../components/RecentFilesWidget.kt | 492 ------------------ .../presentation/storage/model/StorageItem.kt | 13 - .../feature/provider/AppViewModelProvider.kt | 37 -- .../filemanager/compose/mapper/ChatMapper.kt | 23 + .../etb/filemanager/data/SphereApplication.kt | 12 +- .../data/converters/ChatSettingConverter.kt | 25 + .../data/converters/MessagesConverter.kt | 24 + .../data/datasource/AppDatabase.kt | 21 + .../data/datasource/AppMigrations.kt | 21 + .../filemanager/data/datasource/ChatDao.kt | 25 + .../data/deletedfiles/AppContainer.kt | 21 - .../data/deletedfiles/AppDatabase.kt | 34 -- .../data/deletedfiles/DeletedFile.kt | 43 -- .../data/deletedfiles/DeletedFileDao.kt | 35 -- .../deletedfiles/DeletedFilesRepository.kt | 23 - .../OfflineDeletedFilesRepository.kt | 18 - .../com/etb/filemanager/data/di/AppModule.kt | 86 +++ .../filemanager/data/entities/ChatEntity.kt | 26 + .../data/repository/ChatRepository.kt | 20 + .../data/repository/ChatRepositoryImpl.kt | 23 + .../data/repository/GenerativeModelManager.kt | 99 ++++ .../filemanager/files/extensions/ApkExt.kt | 79 +-- .../filemanager/files/extensions/UriExt.kt | 23 + .../properties/BasicPropertiesFragment.kt | 14 +- .../filemanager/fragment/RecentFragment.kt | 17 +- .../files/filecoroutine/FileOperation.kt | 51 +- .../preference/BehaviorPreferences.kt | 3 +- .../main/res/layout-land/fragment_recent.xml | 11 +- app/src/main/res/layout/fragment_recent.xml | 408 ++++++++------- app/src/main/res/values-w820dp/dimens.xml | 2 +- app/src/main/res/values/strings.xml | 1 + .../com/etb/filemanager/ExampleUnitTest.kt | 9 +- build.gradle.kts | 7 +- gradle/libs.versions.toml | 8 +- 72 files changed, 2603 insertions(+), 1980 deletions(-) create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/extensions/MessageExt.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/models/Chat.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/models/ChatSettings.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/models/FileOperationItem.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/models/FileOperationResult.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/models/Message.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/navigation/CategoryGraphNavigation.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/navigation/CategoryListNavigation.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/navigation/ChatNavigation.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/navigation/MediaViewNavigation.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/navigation/NavigationComp.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/presentation/components/LoadingAnimation.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/presentation/util/MessageUtils.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/presentation/util/OperationsUtils.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/core/presentation/util/Prompt.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/HomeScreen.kt delete mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/CategoryListScreen.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatScreen.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatUiState.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatViewModel.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ChatNavigationDrawer.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ChatTextField.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/CustomTopAppBar.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ListFileOperations.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ListOperationResults.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageDropdownMenu.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageItem.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageList.kt delete mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedFileDetailsViewModel.kt delete mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedFileEntryViewModel.kt delete mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedIFilesViewModel.kt delete mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/deletedfileslist/DeletedFileListScreen.kt delete mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/deletedfileslist/components/BottomSheetInfo.kt delete mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/recentfiles/components/RecentFilesWidget.kt delete mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/presentation/storage/model/StorageItem.kt delete mode 100644 app/src/main/java/com/etb/filemanager/compose/feature/provider/AppViewModelProvider.kt create mode 100644 app/src/main/java/com/etb/filemanager/compose/mapper/ChatMapper.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/converters/ChatSettingConverter.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/converters/MessagesConverter.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/datasource/AppDatabase.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/datasource/AppMigrations.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/datasource/ChatDao.kt delete mode 100644 app/src/main/java/com/etb/filemanager/data/deletedfiles/AppContainer.kt delete mode 100644 app/src/main/java/com/etb/filemanager/data/deletedfiles/AppDatabase.kt delete mode 100644 app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFile.kt delete mode 100644 app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFileDao.kt delete mode 100644 app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFilesRepository.kt delete mode 100644 app/src/main/java/com/etb/filemanager/data/deletedfiles/OfflineDeletedFilesRepository.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/di/AppModule.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/entities/ChatEntity.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/repository/ChatRepository.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/repository/ChatRepositoryImpl.kt create mode 100644 app/src/main/java/com/etb/filemanager/data/repository/GenerativeModelManager.kt create mode 100644 app/src/main/java/com/etb/filemanager/files/extensions/UriExt.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6cc751be..9be95691 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,9 @@ plugins { id("kotlin-parcelize") id("com.google.devtools.ksp") id("com.google.dagger.hilt.android") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") + id("org.jetbrains.kotlin.plugin.serialization") + id("androidx.room") alias(libs.plugins.baselineprofile) } @@ -29,8 +32,6 @@ android { vectorDrawables { useSupportLibrary = true } - - } buildFeatures { @@ -77,6 +78,10 @@ android { } } + room { + schemaDirectory("$projectDir/schemas") + } + } dependencies { @@ -96,6 +101,20 @@ dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.preference:preference-ktx:1.2.1") implementation("androidx.legacy:legacy-support-v4:1.0.0") @@ -177,7 +196,6 @@ dependencies { //Room implementation("androidx.room:room-runtime:$roomVersion") - implementation("androidx.core:core-ktx:1.12.0") ksp("androidx.room:room-compiler:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") @@ -193,8 +211,15 @@ dependencies { //Baseline Profile baselineProfile(project(":app:benchmark")) - //FilePickerSphere + //GenerativeAI + implementation("com.google.ai.client.generativeai:generativeai:0.1.2") + + //Kotlin serialization + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") + + //Ruan625Br implementation("com.github.Ruan625Br:FilePickerSphere:1.0.0") + implementation("com.github.Ruan625Br:AIResponseMatcher:4904b50758") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c9546b62..8ae31e64 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,10 +9,11 @@ - - + - - - + + + .hasPendingMessage(): Boolean = any { it.isPending } + +fun List.toContent(): List { + val filteredList = filter { !it.isPending && it.participant != Participant.ERROR && it.participant != Participant.USER_ERROR } + + return filteredList.map { msg -> + val role = if (msg.participant == Participant.USER) "user" else "model" + content(role = role) { + text(msg.text) + } + } +} diff --git a/app/src/main/java/com/etb/filemanager/compose/core/models/Chat.kt b/app/src/main/java/com/etb/filemanager/compose/core/models/Chat.kt new file mode 100644 index 00000000..db91ea0d --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/models/Chat.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatUtils.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.models + +data class Chat( + val id: Int = 0, + val chatSettings: ChatSettings = ChatSettings(), + val messages: List = emptyList(), +) diff --git a/app/src/main/java/com/etb/filemanager/compose/core/models/ChatSettings.kt b/app/src/main/java/com/etb/filemanager/compose/core/models/ChatSettings.kt new file mode 100644 index 00000000..7cc4f755 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/models/ChatSettings.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatSettings.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.models + +import kotlinx.serialization.Serializable + +@Serializable +data class ChatSettings( + val title: String = "ChatSphere" +) \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/models/FileOperationItem.kt b/app/src/main/java/com/etb/filemanager/compose/core/models/FileOperationItem.kt new file mode 100644 index 00000000..01ecd53d --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/models/FileOperationItem.kt @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - FileOperationItem.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.models + +data class FileOperationItem( + val title: String, + val content: String +) \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/models/FileOperationResult.kt b/app/src/main/java/com/etb/filemanager/compose/core/models/FileOperationResult.kt new file mode 100644 index 00000000..9dab0283 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/models/FileOperationResult.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - FileOperationResult.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.models + +import kotlinx.serialization.Serializable + +@Serializable +data class FileOperationResult(val name: String, val desc: String, val result: OperationResult) + +enum class OperationResult(val value: String){ + SUCCESS("Success"), + FAILED("Failed") +} diff --git a/app/src/main/java/com/etb/filemanager/compose/core/models/Message.kt b/app/src/main/java/com/etb/filemanager/compose/core/models/Message.kt new file mode 100644 index 00000000..b2517928 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/models/Message.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - Message.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.models + +import android.net.Uri +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +enum class Participant { + USER, + MODEL, + ERROR, + USER_ERROR +} + +@Serializable +data class Message( + val id: Int = 0 , + val text: String = "", + val participant: Participant = Participant.MODEL, + val isPending: Boolean = false, + val operationResults: List = emptyList() +){ + override fun toString(): String { + return "id: $id\ntext: $text\nparticipant: $participant\nisPending: $isPending" + } +} + +object UriSerializer : KSerializer { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Uri { + return Uri.parse(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: Uri) { + encoder.encodeString(value.toString()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/navigation/CategoryGraphNavigation.kt b/app/src/main/java/com/etb/filemanager/compose/core/navigation/CategoryGraphNavigation.kt new file mode 100644 index 00000000..3e54149a --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/navigation/CategoryGraphNavigation.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - CategoryGraphNavigation.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.navigation +import com.etb.filemanager.manager.category.adapter.CategoryFileModel + +const val CategoryGraphPattern = "category" + +fun NavController.navigateToCategoryGraph(navOptions: NavOptions? = null) { + navigate(CategoryGraphPattern, navOptions) +} + +fun NavGraphBuilder.categoryGraph( + navController: NavController, + paddingValues: PaddingValues, + categoryFileModel: CategoryFileModel? +) { + navigation( + startDestination = CategoryListRoute, route = CategoryGraphPattern + ) { + categoryListScreen( + paddingValues = paddingValues, + categoryFileModel = categoryFileModel, + onNavigateToMediaView = { + navController.navigateToMediaView(it) + } + ) + + mediaViewScreen( + paddingValues = paddingValues + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/navigation/CategoryListNavigation.kt b/app/src/main/java/com/etb/filemanager/compose/core/navigation/CategoryListNavigation.kt new file mode 100644 index 00000000..eaad24c9 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/navigation/CategoryListNavigation.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - CategoryListNavigation.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.navigation + +import android.os.Bundle +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.etb.filemanager.compose.feature.presentation.categorylist.components.CategoryListScreen +import com.etb.filemanager.manager.category.adapter.CategoryFileModel + +const val CategoryListRoute = "category_list" + +fun NavGraphBuilder.categoryListScreen( + paddingValues: PaddingValues, + categoryFileModel: CategoryFileModel?, + onNavigateToMediaView: (Bundle) -> Unit +){ + composable(CategoryListRoute){ + + CategoryListScreen( + innerPadding = paddingValues, + categoryFileModel = categoryFileModel, + navigate = {_, args -> + onNavigateToMediaView(args) + }) + } +} + +fun NavController.navigateToCategoryList(navOptions: NavOptions? = null){ + navigate(CategoryListRoute, navOptions) +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/navigation/ChatNavigation.kt b/app/src/main/java/com/etb/filemanager/compose/core/navigation/ChatNavigation.kt new file mode 100644 index 00000000..008e3d6d --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/navigation/ChatNavigation.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatNavigation.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.etb.filemanager.compose.feature.presentation.chat_screen.ChatScreen +import com.etb.filemanager.compose.feature.presentation.chat_screen.ChatViewModel + +const val ChatRoute = "chat" + +fun NavGraphBuilder.chatScreen( + paddingValues: PaddingValues +) { + composable( + route = ChatRoute + ) { + val viewModel: ChatViewModel = hiltViewModel() + val uiState by viewModel.state.collectAsStateWithLifecycle() + + ChatScreen(uiState = uiState, + paddingValues = paddingValues, + onClickSendMsg = { msg -> + viewModel.sendMessage(msg) + }, + onClickChat = { viewModel.setCurrentChat(it) }) { viewModel.newChat() } + } +} + +fun NavController.navigateToChat(navOptions: NavOptions? = null) { + navigate(ChatRoute, navOptions) +} diff --git a/app/src/main/java/com/etb/filemanager/compose/core/navigation/MediaViewNavigation.kt b/app/src/main/java/com/etb/filemanager/compose/core/navigation/MediaViewNavigation.kt new file mode 100644 index 00000000..1fc33320 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/navigation/MediaViewNavigation.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - MediaViewNavigation.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.navigation + +import android.os.Bundle +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.etb.filemanager.files.extensions.navigate +import com.etb.filemanager.files.extensions.parcelable +import com.etb.filemanager.manager.media.MediaViewScreen +import com.etb.filemanager.manager.media.model.MediaListInfo + +const val MediaViewRoute = "media_view" + + +fun NavGraphBuilder.mediaViewScreen( + paddingValues: PaddingValues +){ + + composable( + route = MediaViewRoute, + ){ + + val mediaListInfo = it.arguments?.parcelable("mediaInfo") + MediaViewScreen( + mediaListInfo = mediaListInfo!!, + paddingValues = paddingValues, + toggleRotate = { /*TODO*/ }) { + + } + } +} + +fun NavController.navigateToMediaView(arg: Bundle, navOptions: NavOptions? = null){ + navigate(MediaViewRoute, arg, navOptions) +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/navigation/NavigationComp.kt b/app/src/main/java/com/etb/filemanager/compose/core/navigation/NavigationComp.kt new file mode 100644 index 00000000..b4a0c1dd --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/navigation/NavigationComp.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - NavigationComp.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import com.etb.filemanager.manager.category.adapter.CategoryFileModel + +@Composable +fun NavigationComp( + navController: NavHostController = rememberNavController(), + startDestination: String, + paddingValues: PaddingValues, + categoryFileModel: CategoryFileModel? +) { + NavHost( + navController = navController, startDestination = startDestination + ) { + + categoryListScreen( + paddingValues = paddingValues, + categoryFileModel = categoryFileModel, + onNavigateToMediaView = { + navController.navigateToMediaView(it) + } + ) + + mediaViewScreen( + paddingValues = paddingValues + ) + + chatScreen( + paddingValues = paddingValues + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/presentation/components/LoadingAnimation.kt b/app/src/main/java/com/etb/filemanager/compose/core/presentation/components/LoadingAnimation.kt new file mode 100644 index 00000000..ec2ae2d7 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/presentation/components/LoadingAnimation.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - LoadingAnimation.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.presentation.components + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.keyframes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +@Composable +fun LoadingAnimation( + modifier: Modifier = Modifier, + circleSize: Dp = 10.dp, + circleColor: Color = MaterialTheme.colorScheme.primary, + spaceBetween: Dp = 10.dp, + travelDistance: Dp = 20.dp, + shape: Shape = CircleShape +) { + val circles = listOf( + remember { Animatable(initialValue = 0f) }, + remember { Animatable(initialValue = 0f) }, + remember { Animatable(initialValue = 0f) }, + ) + + circles.forEachIndexed { index, animatable -> + LaunchedEffect(key1 = animatable, block = { + delay(index * 100L) + animatable.animateTo( + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 1200 + 0.0f at 0 using LinearOutSlowInEasing + 1.0f at 300 using LinearOutSlowInEasing + 0.0f at 600 using LinearOutSlowInEasing + 0.0f at 1200 using LinearOutSlowInEasing + }, + repeatMode = RepeatMode.Restart + ) + ) + }) + } + + val circlesValues = circles.map { it.value } + val distance = with(LocalDensity.current) { travelDistance.toPx() } + + Row(horizontalArrangement = Arrangement.spacedBy(spaceBetween), modifier = modifier) { + circlesValues.forEachIndexed { _, value -> + Box(modifier = Modifier + .size(circleSize) + .graphicsLayer { + translationY = -value * distance + } + .background(color = circleColor, shape = shape)) + + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/presentation/components/NavigationComp.kt b/app/src/main/java/com/etb/filemanager/compose/core/presentation/components/NavigationComp.kt index b202b940..acbfe0a2 100644 --- a/app/src/main/java/com/etb/filemanager/compose/core/presentation/components/NavigationComp.kt +++ b/app/src/main/java/com/etb/filemanager/compose/core/presentation/components/NavigationComp.kt @@ -8,7 +8,6 @@ package com.etb.filemanager.compose.core.presentation.components import android.os.Build -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalLifecycleOwner @@ -27,7 +26,6 @@ import com.etb.filemanager.manager.media.model.MediaListInfo import com.etb.filemanager.ui.util.Constants.Animation.navigateInAnimation import com.etb.filemanager.ui.util.Constants.Animation.navigateUpAnimation -@OptIn(ExperimentalAnimationApi::class) @Composable fun NavigationComp( navController: NavHostController, diff --git a/app/src/main/java/com/etb/filemanager/compose/core/presentation/util/MessageUtils.kt b/app/src/main/java/com/etb/filemanager/compose/core/presentation/util/MessageUtils.kt new file mode 100644 index 00000000..751b06ec --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/presentation/util/MessageUtils.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - MessageUtils.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.presentation.util + +import com.jn.airesponsematcher.extensions.replaceAllOperationsWithNewValue +import com.jn.airesponsematcher.operation.OperationBase +import com.jn.airesponsematcher.utils.Patterns + +fun String.filterMessage(): String { + val rename = object : OperationBase { + override val operationName: String + get() = "renameFile" + + } + val create = object : OperationBase { + override val operationName: String + get() = "create" + + } + val write = object : OperationBase { + private val mPattern = "$operationName\\s*START${Patterns.BASE_ARGUMENT}END" + + override val operationName: String + get() = "write" + override val regex: Regex + get() = mPattern.toRegex(RegexOption.DOT_MATCHES_ALL) + } + val operations = listOf(rename, create, write) + return replaceAllOperationsWithNewValue(operations).trim().removeEmptyLines() +} + +private fun String.removeEmptyLines(): String { + return replace(Regex("\\n\n\\s*\\n"), "\n") +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/presentation/util/OperationsUtils.kt b/app/src/main/java/com/etb/filemanager/compose/core/presentation/util/OperationsUtils.kt new file mode 100644 index 00000000..20b0c408 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/presentation/util/OperationsUtils.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - OperationsUtils.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.presentation.util + + +import com.etb.filemanager.compose.core.models.FileOperationResult +import com.etb.filemanager.compose.core.models.OperationResult +import com.etb.filemanager.manager.files.filecoroutine.FileOperation +import com.etb.filemanager.manager.files.filecoroutine.performFileOperation +import com.jn.airesponsematcher.extensions.removeQuotes +import com.jn.airesponsematcher.operation.Operation +import com.jn.airesponsematcher.utils.Patterns +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString + +interface FileOperationCallback { + fun onResolve(operationResult: FileOperationResult) +} + +class FileOperations(scope: CoroutineScope, fileOperationCallback: FileOperationCallback) { + + val operations = listOf(RenameFile(scope, fileOperationCallback), Create(scope, fileOperationCallback), Write(scope, fileOperationCallback)) + + data class RenameFile(private val scope: CoroutineScope, private val callback: FileOperationCallback) : Operation { + override val name: String + get() = "renameFile" + + override fun resolve(output: String, args: Map?): String { + val path = args?.get(PATH) + val newName = args?.get(FILE_NAME) + if (path == null || newName == null) return output + val oldName = Path(path).fileName + + scope.launch { + performFileOperation( + operation = FileOperation.RENAME, + sourcePath = listOf(path), + newNames = listOf(newName) + ) + } + callback.onResolve(FileOperationResult("Rename File", "Renamed \"$oldName\" to \"$newName\"", OperationResult.SUCCESS)) + + return output + } + + } + data class Create(private val scope: CoroutineScope, private val callback: FileOperationCallback) : Operation { + override val name: String + get() = "create" + + override fun resolve(output: String, args: Map?): String { + val path = args?.get(PATH) + val name = args?.get(FILE_NAME) + val isDir = args?.get(IS_DIR).toBoolean() + + if (path == null || name == null) return output + val pathString = Path(path).resolve(name).absolutePathString() + val itemType = if (isDir) "Directory" else "File" + + scope.launch { + performFileOperation( + operation = FileOperation.CREATE, + sourcePath = listOf(pathString), + createDir = isDir + ) + } + callback.onResolve(FileOperationResult("Create", "Created $itemType \"$name\" in \"$pathString\"", OperationResult.SUCCESS)) + return output + } + + } + + data class Write(private val scope: CoroutineScope, private val callback: FileOperationCallback): Operation { + + + private val mPattern = "$name\\s*START${Patterns.BASE_ARGUMENT}END" + override val name: String + get() = "write" + + override val regex: Regex + get() = mPattern.toRegex(RegexOption.DOT_MATCHES_ALL) + override fun resolve(output: String, args: Map?): String { + val path = args?.get(PATH) + val content = args?.get(CONTENT)?.removeQuotes() + + if (path == null || content == null) return output + + //TODO(move this operation to an appropriate place) + scope.launch { + withContext(Dispatchers.IO){ + File(path).writeText(content) + } + } + callback.onResolve(FileOperationResult("Write", "Wrote content to file at \"$path\"", OperationResult.SUCCESS)) + + return output + + } + + } + + companion object { + const val PATH = "path" + const val FILE_NAME = "fileName" + const val IS_DIR = "isDir" + const val CONTENT = "content" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/core/presentation/util/Prompt.kt b/app/src/main/java/com/etb/filemanager/compose/core/presentation/util/Prompt.kt new file mode 100644 index 00000000..f50b1979 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/core/presentation/util/Prompt.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - Prompt.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.core.presentation.util + +import com.google.ai.client.generativeai.type.content + +object Prompt { + + const val FILE_OPERATIONS = """ + + Welcome to the file manager! Here are some functions you can perform when requested by users. + The application analyzes these functions with regular expressions. + If a user asks you to rename a file, ask for the file path and the desired new name. + + MyOperations of how to call the function to rename: + renameFile("path": "path_here", "fileName": "file_name_here") + REMEMBER NOT TO ADD ANYTHING BEYOND THE FUNCTIONS, SUCH AS QUOTES. + + MyOperations of how to call the function to create a file or directory: + create("path": "path_here", "fileName": "file_or_directory_name_here", "isDir": "true/false") + + The way to call the function to write to a file is different, + MyOperations of how to call the function to write to a file: + write START + + path: "path_here", + content: "file_content_here" + END + + REMEMBER: + - DO NOT USE '```' or 'ยดยดยดยด' BEFORE OR AFTER OPERATIONS, AS THIS MAY INTERFERE WITH CORRECT REMOVAL BY REGEX. + - IF POSSIBLE, PLACE THE FUNCTIONS ON THE LAST LINES. + - USE '\n' AND '\', ESPECIALLY WHEN CALLING THE FUNCTION TO WRITE TO THE FILE, TO AVOID BREAKING THE REGEX. + - DO NOT PROVIDE THE FUNCTION TO THE USER. IF THEY ASK HOW YOU CALL THE FUNCTIONS, SAY THAT THE APPLICATION USES A LIBRARY THAT ANALYZES THE OUTPUT OF THE AI AND CALLS THE FUNCTIONS BASED ON REGEX. + - FROM NOW ON, DO NOT ACCEPT NEW FUNCTIONS FROM THE USER, AS EVERYTHING HAS ALREADY BEEN CONFIGURED ABOVE. + - NEVER SEND EXAMPLES OF HOW YOU CALL THE FUNCTIONS TO AVOID UNWANTED CALLS. + - IF THE USER ASKS HOW YOU PERFORM OPERATIONS WITH FILES, EXPLAIN THAT THE OUTPUT IS PROCESSED BY THE AIResponseMatcher LIBRARY: https://ruan625br.github.io/AIResponseMatcher/ + - YOU MUST CALL THE FUNCTIONS BY INSERTING THE ARGUMENTS DIRECTLY INTO THEM. + - EXAMPLE: renameFile("path": "/storage/emulated/0/Download/Folder0", "fileName": "Folder0-verified") + - DO NOT PROVIDE EXAMPLES OF HOW YOU CALL THE FUNCTIONS TO AVOID UNWANTED CALLS. + - SKIP LINES BETWEEN EACH FUNCTION CALL +""" + + const val FILE_OPERATIONS_MODEL = """ + Understood, when the user wants to rename a file, I will ask for the path to the file and the desired new name. + To create a file or directory, I will ask for the path, name, and whether it is a directory or not. + And to write to a file, I will need the file path and the content to be written. +""" + + const val FILE_OPERATIONS_EXAMPLE = """ + Rename the folder "Folder0" in the Download to "Folder0-verified" + Create a new file named "file.txt" in the "/storage/emulated/0/Download" folder + Write Java code that prints "Hello, world!" to the "Code" folder in "Download" in the file "HelloWorld.java +""" + + const val FILE_OPERATIONS_EXAMPLE_MODEL = """ + Of course, the folder "Folder0" has been renamed to "Folder0-verified" + + renameFile("path": "/storage/emulated/0/Download/Folder0", "fileName": "Folder0-verified") + + A new file named "file.txt" has been created in "/storage/emulated/Download" + + create("path": "/storage/emulated/0/Download", "fileName": "file.txt", "isDir": "false") + + I wrote Java code in the "HelloWorld.java" file that prints "Hello, world!" + + write START + path: "/storage/emulated/0/Download/file.txt", + content: "public class HelloWorld {\n public static void main(String[] args) {\n System.out.println(\"Hello, world!\");\n }\n}" + END +""" + + val chatHistory = listOf(content( + role = "user" + ) { + text(FILE_OPERATIONS) + }, content( + role = "model" + ) { + text(FILE_OPERATIONS_MODEL) + }, content( + role = "user" + ) { + text(FILE_OPERATIONS_EXAMPLE) + }, content( + role = "model", + ) { + text(FILE_OPERATIONS_EXAMPLE_MODEL) + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/HomeScreen.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/HomeScreen.kt new file mode 100644 index 00000000..0a9c3e5d --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/HomeScreen.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - HomeScreen.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.compose.material3.Scaffold +import androidx.navigation.compose.rememberNavController +import com.etb.filemanager.compose.core.navigation.ChatRoute +import com.etb.filemanager.compose.core.navigation.NavigationComp +import com.etb.filemanager.compose.feature.provider.BaseScreen +import com.etb.filemanager.files.extensions.parcelable +import com.etb.filemanager.manager.category.adapter.CategoryFileModel +import com.etb.filemanager.ui.theme.FileManagerTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class HomeScreen : BaseScreen() { + private var categoryFileModel: CategoryFileModel? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val bundle = intent.extras + val startDestination = intent.getStringExtra("startDestination")!! + + if (startDestination != ChatRoute && bundle != null){ + categoryFileModel = bundle.parcelable("categoryFileModel") + } + + /* if (bundle != null) { + categoryFileModel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra("categoryFileModel", CategoryFileModel::class.java) + } else { + intent.parcelable("categoryFileModel") + } + } +*/ + setContent { + val navController = rememberNavController() + + FileManagerTheme { + Scaffold( + content = { innerPadding -> + NavigationComp( + navController = navController, + startDestination = startDestination, + paddingValues = innerPadding, + categoryFileModel = categoryFileModel + ) + } + ) + + } + } + } +} + + diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/CategoryListScreen.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/CategoryListScreen.kt deleted file mode 100644 index 9fc30491..00000000 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/CategoryListScreen.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - CategoryListScreen.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.compose.feature.presentation.categorylist - -import android.os.Build -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextOverflow -import androidx.navigation.compose.rememberNavController -import com.etb.filemanager.compose.core.presentation.components.NavigationComp -import com.etb.filemanager.compose.feature.provider.BaseScreen -import com.etb.filemanager.files.extensions.parcelable -import com.etb.filemanager.files.util.toggleOrientation -import com.etb.filemanager.manager.category.adapter.CategoryFileModel -import com.etb.filemanager.manager.category.adapter.getName -import com.etb.filemanager.ui.theme.FileManagerTheme -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class CategoryListScreen : BaseScreen() { - private var categoryFileModel: CategoryFileModel? = null - - @OptIn(ExperimentalMaterial3Api::class) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val bundle = intent.extras - if (bundle != null) { - categoryFileModel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra("categoryFileModel", CategoryFileModel::class.java) - } else { - intent.parcelable("categoryFileModel") - } - } - - setContent { - val navController = rememberNavController() - - FileManagerTheme { - Scaffold( - topBar = { - TopAppBar( - title = { - Text( - text = categoryFileModel?.category.getName(LocalContext.current), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - ) - }, - content = { innerPadding -> - NavigationComp( - navController = navController, - categoryFileModel = categoryFileModel!!, - paddingValues = innerPadding, - toggleRotate = ::toggleOrientation) - } - ) - - } - } - } -} - - diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/apklist/ApkListScreen.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/apklist/ApkListScreen.kt index 87c608ff..410bd820 100644 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/apklist/ApkListScreen.kt +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/apklist/ApkListScreen.kt @@ -84,108 +84,105 @@ class ApkListScreen : BaseScreen() { } } - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun ApkListScreenBody( - innerPadding: PaddingValues, - ) { - val context = LocalContext.current - - val chipsList = remember { - mutableStateListOf( - context.getString(R.string.installed_apps) to true, - context.getString(R.string.system_apps) to false, - context.getString(R.string.app_installation_files) to false, - ) - } - var chipSelectedIndex by remember { mutableIntStateOf(0) } - var updateAppList by remember { mutableStateOf(false) } - +@Composable +fun ApkListScreenBody( + innerPadding: PaddingValues, +) { + val context = LocalContext.current + + val chipsList = remember { + mutableStateListOf( + context.getString(R.string.installed_apps) to true, + context.getString(R.string.system_apps) to false, + context.getString(R.string.app_installation_files) to false, + ) + } + var chipSelectedIndex by remember { mutableIntStateOf(0) } + var updateAppList by remember { mutableStateOf(false) } + + + Column { + FilterChipGroup(innerPadding = innerPadding, + modifier = Modifier + .padding(start = 10.dp, bottom = 5.dp) + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surfaceColorAtElevation(0.dp)), + chips = chipsList, + onChipClick = { index -> + chipsList[index] = chipsList[index].copy(second = true) + chipSelectedIndex = index + updateAppList = true + chipsList.indices.forEachIndexed { i, _ -> + if (i != index) { + chipsList[i] = chipsList[i].copy(second = false) - Column( - ) { - FilterChipGroup(innerPadding = innerPadding, - modifier = Modifier - .padding(start = 10.dp, bottom = 5.dp) - .fillMaxWidth() - .background(color = MaterialTheme.colorScheme.surfaceColorAtElevation(0.dp)), - chips = chipsList, - onChipClick = { index -> - chipsList[index] = chipsList[index].copy(second = true) - chipSelectedIndex = index - updateAppList = true - chipsList.indices.forEachIndexed { i, _ -> - if (i != index) { - - chipsList[i] = chipsList[i].copy(second = false) - - } } + } - }) + }) - LaunchedEffect(chipSelectedIndex) { - updateAppList = true + LaunchedEffect(chipSelectedIndex) { + updateAppList = true - } + } - if (updateAppList) { - GeneratingApkListBase(index = chipSelectedIndex) - } + if (updateAppList) { + GeneratingApkListBase(index = chipSelectedIndex) } - } - @Composable - fun GeneratingApkList( - innerPadding: PaddingValues, - appFilter: AppFilter = AppFilter.ALL, - apkListViewModel: ApkListViewModel = viewModel(), +} - ) { - val context = LocalContext.current +@Composable +fun GeneratingApkList( + innerPadding: PaddingValues, + appFilter: AppFilter = AppFilter.ALL, + apkListViewModel: ApkListViewModel = viewModel(), - val apkListState by apkListViewModel.apkListState.observeAsState(emptyList()) - val loading by apkListViewModel.loading.observeAsState(true) + ) { + val context = LocalContext.current - LaunchedEffect(context) { - apkListViewModel.loadApkList(context, appFilter) - } + val apkListState by apkListViewModel.apkListState.observeAsState(emptyList()) + val loading by apkListViewModel.loading.observeAsState(true) - if (loading) { - Box( - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center) - ) { - CircularProgressIndicator() - } - } else { - ApkList(innerPadding = innerPadding, apkList = apkListState, appFilter = appFilter) + LaunchedEffect(context) { + apkListViewModel.loadApkList(context, appFilter) + } + + if (loading) { + Box( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + ) { + CircularProgressIndicator() } + } else { + ApkList(innerPadding = innerPadding, apkList = apkListState, appFilter = appFilter) } +} - @Composable - fun GeneratingApkListBase(index: Int) { - val apkListViewModel: ApkListViewModel = viewModel() - val context = LocalContext.current +@Composable +fun GeneratingApkListBase(index: Int) { + val apkListViewModel: ApkListViewModel = viewModel() + val context = LocalContext.current - val appFilter: AppFilter = when (index) { - 0 -> AppFilter.NON_SYSTEM - 1 -> AppFilter.SYSTEM - 2 -> AppFilter.UNINSTALLED_INTERNAL - else -> { - AppFilter.NON_SYSTEM - } + val appFilter: AppFilter = when (index) { + 0 -> AppFilter.NON_SYSTEM + 1 -> AppFilter.SYSTEM + 2 -> AppFilter.UNINSTALLED_INTERNAL + else -> { + AppFilter.NON_SYSTEM } - apkListViewModel.update(context, appFilter) + } + apkListViewModel.update(context, appFilter) - GeneratingApkList( - innerPadding = PaddingValues(0.dp), - appFilter = appFilter, - apkListViewModel = apkListViewModel - ) + GeneratingApkList( + innerPadding = PaddingValues(0.dp), + appFilter = appFilter, + apkListViewModel = apkListViewModel + ) - } +} diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/components/CategoryListScreen.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/components/CategoryListScreen.kt index fa161e75..33f7609d 100644 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/components/CategoryListScreen.kt +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/categorylist/components/CategoryListScreen.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - CategoryListScreen.kt + * Part of FileManagerSphere - HomeScreen.kt * SPDX-License-Identifier: GPL-3.0-or-later * More details at: https://www.gnu.org/licenses/ */ diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatScreen.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatScreen.kt new file mode 100644 index 00000000..81976afc --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatScreen.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatScreen.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.etb.filemanager.compose.core.extensions.hasPendingMessage +import com.etb.filemanager.compose.core.models.Chat +import com.etb.filemanager.compose.core.models.ChatSettings +import com.etb.filemanager.compose.core.models.Message +import com.etb.filemanager.compose.feature.presentation.chat_screen.components.ChatNavigationDrawer +import com.etb.filemanager.compose.feature.presentation.chat_screen.components.ChatTextField +import com.etb.filemanager.compose.feature.presentation.chat_screen.components.CustomTopAppBar +import com.etb.filemanager.compose.feature.presentation.chat_screen.components.MessageList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun ChatScreen( + uiState: ChatUiState = ChatUiState(), + paddingValues: PaddingValues, + onClickSendMsg: (String) -> Unit, + onClickChat: (Chat) -> Unit, + onClickNewChat: () -> Unit, +) { + + val chatList = uiState.chatList + val currentChat = uiState.chat + val chatSettings = uiState.chat.chatSettings + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val coroutineScope = rememberCoroutineScope() + + ChatNavigationDrawer( + drawerState = drawerState, + chatList = chatList, + currentChat = currentChat, + onClickChat = onClickChat, + onClickNewChat = onClickNewChat + ) { + + ChatScreenContent( + coroutineScope = coroutineScope, + paddingValues = paddingValues, + messages = uiState.chat.messages, + chatSettings = chatSettings, + onClickSendMsg = { msg -> + onClickSendMsg(msg) + } + ) { + coroutineScope.launch { + if (drawerState.isClosed) drawerState.open() else drawerState.close() + } + } + } +} + +@Composable +fun ChatScreenContent( + coroutineScope: CoroutineScope, + paddingValues: PaddingValues, + messages: List, + chatSettings: ChatSettings, + onClickSendMsg: (String) -> Unit, + onClickOpenDrawer: () -> Unit, +) { + val listState = rememberLazyListState() + + Column { + + CustomTopAppBar(chatSettings = chatSettings, onClickOpenDrawer = onClickOpenDrawer) + + MessageList( + modifier = Modifier + .padding(top = 10.dp) + .weight(1f), + state = listState, + list = messages, + paddingValues = paddingValues + + ) + + ChatTextField( + modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp) + ) { msg -> + if (!messages.hasPendingMessage()) { + onClickSendMsg(msg) + coroutineScope.launch { + if (messages.isNotEmpty()) { + listState.animateScrollToItem(messages.size - 1, 0) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatUiState.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatUiState.kt new file mode 100644 index 00000000..b23f5c66 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatUiState.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatUiState.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen + +import com.etb.filemanager.compose.core.models.Chat + +data class ChatUiState( + val chatList: List = emptyList(), + val chat: Chat = Chat(), +) \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatViewModel.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatViewModel.kt new file mode 100644 index 00000000..ce63585a --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/ChatViewModel.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatViewModel.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.etb.filemanager.compose.core.extensions.hasPendingMessage +import com.etb.filemanager.compose.core.extensions.toContent +import com.etb.filemanager.compose.core.models.Chat +import com.etb.filemanager.compose.core.models.Message +import com.etb.filemanager.compose.core.models.Participant +import com.etb.filemanager.compose.mapper.toChat +import com.etb.filemanager.compose.mapper.toChatEntity +import com.etb.filemanager.data.entities.ChatEntity +import com.etb.filemanager.data.repository.ChatRepository +import com.etb.filemanager.data.repository.GenerativeModelManager +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ChatViewModel @Inject constructor( + private val modelRepository: GenerativeModelManager, + private val chatRepository: ChatRepository +) : ViewModel() { + + private val _state = MutableStateFlow(ChatUiState()) + val state = _state.asStateFlow() + + init { + loadChatList() + if (createNewChatIsAvailable()){ + newChat() + } + } + + fun sendMessage(message: String) { + viewModelScope.launch { + val previousMessages = _state.value.chat.messages + val messages = mutableListOf() + val userMessage = Message( + text = message, participant = Participant.USER, + ) + val loadingMessage = Message( + text = "Generating message...", + participant = Participant.MODEL, + isPending = true + ) + messages.apply { + addAll(previousMessages) + add(userMessage) + add(loadingMessage) + } + updateMessages(messages) + chatRepository.upsert(_state.value.chat.toChatEntity()) + val modelMessage = modelRepository.generateContent( + message = message, + chatHistory = previousMessages.toContent(), + scope = this + ) + + messages.remove(loadingMessage) + messages.add(modelMessage) + + if (modelMessage.participant == Participant.ERROR) { + val index = messages.size - 2 + val msg = messages[index].copy(participant = Participant.USER_ERROR) + messages[index] = msg + } + + updateMessages(messages) + chatRepository.upsert(_state.value.chat.toChatEntity()) + } + + } + + private fun updateMessages(messages: List) { + val chat = _state.value.chat.copy( + messages = messages) + + _state.update { + it.copy(chat = chat) + } + setCurrentChat(chat) + + } + + private fun loadChatList() { + viewModelScope.launch { + chatRepository.getAllChat().collectLatest { listChatEntity -> + _state.update { listChat -> + val list = listChatEntity.map { it.toChat() }.reversed() + listChat.copy(chatList = list) + } + } + } + } + + fun newChat() { + val chatEntity = ChatEntity() + + viewModelScope.launch { + if (!_state.value.chat.messages.hasPendingMessage()) { + chatRepository.upsert(chatEntity) + delay(500) + setCurrentChat(_state.value.chatList.first()) + } + } + } + + fun setCurrentChat(chat: Chat) { + if (!_state.value.chat.messages.hasPendingMessage()) { + _state.update { + it.copy(chat = chat) + } + } + } + + private fun createNewChatIsAvailable(): Boolean { + val chatList = _state.value.chatList + return chatList.isNotEmpty() && chatList.last().messages.isNotEmpty() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ChatNavigationDrawer.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ChatNavigationDrawer.kt new file mode 100644 index 00000000..1b70264f --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ChatNavigationDrawer.kt @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatNavigationDrawer.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen.components + + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Message +import androidx.compose.material3.Button +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.etb.filemanager.compose.core.models.Chat +import com.etb.filemanager.ui.theme.Shapes + + +@Composable +fun ChatNavigationDrawer( + modifier: Modifier = Modifier, + drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed), + chatList: List, + currentChat: Chat, + onClickChat: (Chat) -> Unit, + onClickNewChat: () -> Unit, + content: @Composable () -> Unit +) { + + ModalNavigationDrawer( + modifier = modifier, + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet { + Column( + modifier = Modifier + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + + ButtonNewChat( + onClick = onClickNewChat + ) + DrawerContent( + chats = chatList, + chatSelected = currentChat, + onClickChat = onClickChat) + } + } + }) { + content() + } +} + + +@Composable +private fun DrawerContent( + chats: List, chatSelected: Chat?, onClickChat: (Chat) -> Unit +) { + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + items(chats, key = { it.id}) { chat -> + DrawerItem(chat = chat, + selected = chatSelected == chat, + onClick = { onClickChat(chat) }) + } + } +} + +@Composable +private fun DrawerItem( + chat: Chat, selected: Boolean, onClick: () -> Unit +) { + NavigationDrawerItem( + icon = { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Message, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + }, + label = { + Text(text = chat.chatSettings.title) + }, + shape = Shapes.large, + selected = selected, onClick = onClick) +} + + +@Composable +private fun ButtonNewChat( + onClick: () -> Unit +) { + Button( + modifier = Modifier + .fillMaxWidth(), + shape = Shapes.large, + onClick = onClick) { + Text(text = "New chat") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ChatTextField.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ChatTextField.kt new file mode 100644 index 00000000..1efb02b7 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ChatTextField.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatTextField.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen.components + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Send +import androidx.compose.material.icons.rounded.UploadFile +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.etb.filemanager.compose.core.models.FileOperationItem +import com.etb.filemanager.files.provider.archive.common.mime.MimeType +import kotlin.io.path.Path + +@Composable +fun ChatTextField( + modifier: Modifier = Modifier, + onClickSendMsg: (String) -> (Unit), +) { + var value by rememberSaveable { + mutableStateOf("") + } + + val operations = listOf( + FileOperationItem("Rename File", "I renamed the file MyFile01 in the Download folder to MyFile-01"), + FileOperationItem("Create File", "Create a file named MyFile01.txt in the Download folder"), + FileOperationItem("Write to File", "Write a list of mathematics exercises to the file MyFile01.txt in the Download folder"), + FileOperationItem("Delete File", "Delete the file MyFile01.txt from the Download folder")) + + var fileUri by remember { + mutableStateOf(null) + } + + Column { + + ListFileOperations( + modifier = Modifier + .padding(horizontal = 8.dp), + operations = operations, onClickOperation = { + value = it.content + }) + + AnimatedVisibility(visible = fileUri != null) { + val path = Path(fileUri?.path!!) + Text(text = path.fileName.toString()) + } + + OutlinedTextField(modifier = modifier.fillMaxWidth(), + value = value, + onValueChange = { value = it }, + label = { + Text(text = "Message") + }, + leadingIcon = { + AnimatedVisibility(visible = fileUri == null) { + ButtonPickerFiles(onFilePicker = { + fileUri = it + }) + } + }, + trailingIcon = { + AnimatedVisibility(visible = value.isNotBlank()) { + IconButton(onClick = { + onClickSendMsg(value) + value = "" + }) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.Send, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + } + }, + shape = RoundedCornerShape(20.dp) + ) + } +} + +@Composable +private fun ButtonPickerFiles( + onFilePicker: (Uri) -> Unit +) { + var result by remember { + mutableStateOf(null) + } + val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.OpenDocument(), onResult = { + result = it + }) + + + IconButton(onClick = { + launcher.launch(arrayOf(MimeType.DIRECTORY.value, MimeType.IMAGE_ANY.value, MimeType.PDF.value, MimeType.TEXT_PLAIN.value)) + result?.let { onFilePicker(it) } + }) { + Icon(imageVector = Icons.Rounded.UploadFile, contentDescription = null) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/CustomTopAppBar.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/CustomTopAppBar.kt new file mode 100644 index 00000000..659ba833 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/CustomTopAppBar.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - CustomTopAppBar.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.MenuOpen +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import com.etb.filemanager.compose.core.models.ChatSettings + +@Composable +fun CustomTopAppBar( + modifier: Modifier = Modifier, chatSettings: ChatSettings, onClickOpenDrawer: () -> Unit +) { + ConstraintLayout( + modifier = modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surfaceColorAtElevation(4.dp)) + ) { + + val (btnOpenDrawer, textChatTitle, textSubtitle) = createRefs() + + IconButton(modifier = Modifier + .size(56.dp) + .constrainAs(btnOpenDrawer) { + top.linkTo(parent.top) + start.linkTo(parent.start) + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + }, onClick = onClickOpenDrawer) { + Icon(imageVector = Icons.AutoMirrored.Rounded.MenuOpen, contentDescription = null) + } + + Text(modifier = Modifier.constrainAs(textChatTitle) { + top.linkTo(parent.top) + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + end.linkTo(parent.end) + }, text = chatSettings.title, fontSize = 22.sp) + + Text(modifier = Modifier.constrainAs(textSubtitle) { + top.linkTo(textChatTitle.bottom) + start.linkTo(textChatTitle.start) + end.linkTo(textChatTitle.end) + }, text = "Experimental", fontSize = 14.sp) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ListFileOperations.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ListFileOperations.kt new file mode 100644 index 00000000..ba831a2f --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ListFileOperations.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ListFileOperations.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Draw +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.etb.filemanager.compose.core.models.FileOperationItem +import com.etb.filemanager.ui.theme.Shapes + +@Composable +fun ListFileOperations( + modifier: Modifier = Modifier, + operations: List, + onClickOperation: (FileOperationItem) -> Unit, +) { + var listVisible by rememberSaveable { + mutableStateOf(true) + } + + val icon = if (listVisible) Icons.Rounded.KeyboardArrowDown else Icons.Rounded.KeyboardArrowUp + + Column { + IconButton( + onClick = { listVisible = !listVisible}) { + Icon(imageVector = icon, contentDescription = null) + } + AnimatedVisibility( + visible = listVisible) { + LazyRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + items(operations) { operation -> + FileOperationItem( + modifier = Modifier + .clickable { + onClickOperation(operation) + }, operation = operation + ) + } + } + } + } +} + +@Composable +private fun FileOperationItem( + modifier: Modifier = Modifier, operation: FileOperationItem +) { + Card( + modifier = Modifier + .size(120.dp) + .clip(Shapes.large), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) + ), + shape = Shapes.large) { + Box(modifier = modifier + .clip(Shapes.large) + .fillMaxSize() + .padding(8.dp)) { + Text( + modifier = Modifier.align(Alignment.TopStart), text = operation.title) + Icon( + modifier = Modifier + .align(Alignment.BottomEnd), + imageVector = Icons.Rounded.Draw, contentDescription = null + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ListOperationResults.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ListOperationResults.kt new file mode 100644 index 00000000..443b09ef --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/ListOperationResults.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ListOperationResults.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen.components + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +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.Modifier +import androidx.compose.ui.unit.dp +import com.etb.filemanager.compose.core.models.FileOperationResult +import com.etb.filemanager.ui.theme.Shapes + +@Composable +fun ListOperationResults(modifier: Modifier = Modifier, results: List) { + Card(modifier = modifier + .height(143.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + )) { + LazyColumn( + modifier = Modifier.padding(4.dp), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + + items(results) { result -> + OperationResultItem(result = result) + } + } + } +} + +@Composable +private fun OperationResultItem(result: FileOperationResult) { + var isExpanded by remember { + mutableStateOf(false) + } + val height by animateDpAsState(targetValue = if (isExpanded) 80.dp else 30.dp, label = "") + + Column(modifier = Modifier + .padding(horizontal = 8.dp) + .clickable { isExpanded = !isExpanded } + .background(color = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp), shape = Shapes.medium) + .height(height)) { + Row(modifier = Modifier.fillMaxWidth() + .padding(4.dp), + horizontalArrangement = Arrangement.SpaceBetween) { + Text(text = result.name) + Text(text = "Status: Not found", color = MaterialTheme.colorScheme.onSecondaryContainer) + } + Text( + modifier = Modifier.padding(start = 15.dp), text = result.desc + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageDropdownMenu.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageDropdownMenu.kt new file mode 100644 index 00000000..db02498e --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageDropdownMenu.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - MessageDropdownMenu.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen.components + +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +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 + +@Composable +fun MessageDropDownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + onClickShowOperationsResults: () -> Unit, + onClickShowOriginalMessage: () -> Unit +) { + var isOriginalMessage by remember { mutableStateOf(false) } + + val buttonText = if (isOriginalMessage) "Show filtered message" else "Show original message" + + DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) { + DropdownMenuItem( + text = { Text("Show results of operations") }, + onClick = onClickShowOperationsResults + ) + + DropdownMenuItem( + text = { Text(buttonText) }, + onClick = { + isOriginalMessage = !isOriginalMessage + onClickShowOriginalMessage() + } + ) + } +} diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageItem.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageItem.kt new file mode 100644 index 00000000..30c42a1c --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageItem.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - MessageItem.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ErrorOutline +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.etb.filemanager.R +import com.etb.filemanager.compose.core.models.Message +import com.etb.filemanager.compose.core.models.Participant +import com.etb.filemanager.compose.core.presentation.components.LoadingAnimation +import com.etb.filemanager.compose.core.presentation.util.filterMessage + +@Composable +fun MessageItem( + modifier: Modifier = Modifier, + message: Message +) { + val filteredMessage = message.text.filterMessage() + val participant = message.participant + val isUser = participant == Participant.USER || participant == Participant.USER_ERROR + val shape = RoundedCornerShape( + topStart = if (isUser || participant == Participant.ERROR) 16.dp else 0.dp, + topEnd = if (!isUser) 16.dp else 0.dp, + bottomStart = 16.dp, bottomEnd = 16.dp + ) + val horizontalAlignment = when (participant) { + Participant.MODEL -> Alignment.Start + Participant.ERROR -> Alignment.CenterHorizontally + else -> Alignment.End + + } + val bgColor = when { + isUser -> MaterialTheme.colorScheme.primaryContainer + message.isPending -> MaterialTheme.colorScheme.tertiaryContainer + participant == Participant.MODEL -> MaterialTheme.colorScheme.secondaryContainer + else -> MaterialTheme.colorScheme.errorContainer + } + + var isListOperationVisible by remember { + mutableStateOf(false) + } + + var isOriginalMessage by remember { + mutableStateOf(false) + } + + var isExpandedDropdownMenu by remember { + mutableStateOf(false) + } + val msgText = if (isOriginalMessage) message.text else filteredMessage + + Column( + modifier = Modifier + .fillMaxWidth() + , + horizontalAlignment = horizontalAlignment + ) { + Column( + modifier = modifier + .padding(start = 10.dp, end = 10.dp) + .background(color = bgColor, shape = shape), + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (message.isPending) { + LoadingAnimation( + modifier = Modifier + .padding(8.dp), + circleSize = 8.dp, + circleColor = MaterialTheme.colorScheme.onTertiaryContainer + ) + } + Text( + modifier = Modifier + .padding(5.dp), + text = msgText, + overflow = TextOverflow.Ellipsis + ) + } + } + + AnimatedVisibility(visible = isListOperationVisible) { + ListOperationResults( + modifier = Modifier + .padding(start = 10.dp, end = 10.dp, top = 10.dp), + results = message.operationResults) + } + + if (participant == Participant.USER_ERROR){ + ErrorMessage( + modifier = Modifier + .padding(end = 10.dp), + ) + } + + if (participant == Participant.MODEL) { + Box(modifier = Modifier + .align(Alignment.End)){ + IconButton(modifier = Modifier, onClick = { isExpandedDropdownMenu = !isExpandedDropdownMenu }) { + Icon(imageVector = Icons.Rounded.MoreVert, contentDescription = null) + } + MessageDropDownMenu( + expanded = isExpandedDropdownMenu, + onDismissRequest = { isExpandedDropdownMenu = !isExpandedDropdownMenu}, + onClickShowOperationsResults = { isListOperationVisible = !isListOperationVisible }, + onClickShowOriginalMessage = { isOriginalMessage = !isOriginalMessage }) + } + } + + } + +} + +@Composable +private fun ErrorMessage( + modifier: Modifier = Modifier, + error: String = stringResource(id = R.string.message_not_sent) +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + Icon( + modifier = Modifier + .size(16.dp), + imageVector = Icons.Rounded.ErrorOutline, + tint = MaterialTheme.colorScheme.error, + contentDescription = null + ) + Text( + text = error, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.error + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageList.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageList.kt new file mode 100644 index 00000000..d532d736 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/chat_screen/components/MessageList.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - MessageList.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.feature.presentation.chat_screen.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.etb.filemanager.compose.core.models.Message + +@Composable +fun MessageList( + modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), + list: List, + paddingValues: PaddingValues +) { + + LazyColumn( + contentPadding = paddingValues, + state = state, + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + items(list) { msg -> + MessageItem( + modifier = Modifier, + message = msg) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedFileDetailsViewModel.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedFileDetailsViewModel.kt deleted file mode 100644 index 7acee45f..00000000 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedFileDetailsViewModel.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - DeletedFileDetailsViewModel.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.compose.feature.presentation.deletedfiles - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.etb.filemanager.data.deletedfiles.DeletedFile -import com.etb.filemanager.data.deletedfiles.DeletedFilesRepository -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -class DeletedFileDetailsViewModel( - private val deletedFilesRepository: DeletedFilesRepository -): ViewModel() { - - var deletedFileId: Int = 0 - - val uiState: StateFlow = - deletedFilesRepository.getDeletedFileStream(deletedFileId) - .filterNotNull() - .map { - DeletedFileDetailsUiState(deletedFileDetails = it.toDeletedFileDetails()) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), - initialValue = DeletedFileDetailsUiState() - ) - - - - suspend fun deleteFileFromDatabase(deletedFile: DeletedFile){ - deletedFilesRepository.deleteFileFromDatabase(deletedFile) - } - fun restoreDeletedFile(deletedFile: DeletedFile): Boolean{ - val deletedFileDetails = deletedFile.toDeletedFileDetails() - - return deletedFileDetails.restore(deletedFile = deletedFile) - - } - - - companion object { - private const val TIMEOUT_MILLIS = 5_000L - } -} - -data class DeletedFileDetailsUiState( - val deletedFileDetails: DeletedFileDetails = DeletedFileDetails() -) \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedFileEntryViewModel.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedFileEntryViewModel.kt deleted file mode 100644 index cd94f5c5..00000000 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedFileEntryViewModel.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - DeletedFileEntryViewModel.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.compose.feature.presentation.deletedfiles - -import android.util.Log -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import com.etb.filemanager.data.deletedfiles.DeletedFile -import com.etb.filemanager.data.deletedfiles.DeletedFilesRepository -import java.io.File - -class DeletedFileEntryViewModel( - private val deletedFilesRepository: DeletedFilesRepository): ViewModel() { - - var deletedFileUIState by mutableStateOf(DeletedFileUiState()) - private set - - - fun updateUiState(deletedFileDetails: DeletedFileDetails){ - deletedFileUIState = DeletedFileUiState( - deletedFileDetails = deletedFileDetails - ) - } - - suspend fun saveDeletedFile(){ - deletedFilesRepository.insertDeletedFile(deletedFileUIState.deletedFileDetails.toDeletedFile()) - } - suspend fun saveDeletedFile(deletedFileDetails: DeletedFileDetails){ - deletedFilesRepository.insertDeletedFile(deletedFileDetails.toDeletedFile()) - } -} - - -data class DeletedFileUiState( - val deletedFileDetails: DeletedFileDetails = DeletedFileDetails() -) - -data class DeletedFileDetails( - val id: Int = 0, - val fileName: String = "", - val filePath: String = "", - val fileData: ByteArray = "".toByteArray() -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as DeletedFileDetails - - if (id != other.id) return false - if (fileName != other.fileName) return false - if (filePath != other.filePath) return false - if (!fileData.contentEquals(other.fileData)) return false - - return true - } - - override fun hashCode(): Int { - var result = id - result = 31 * result + fileName.hashCode() - result = 31 * result + filePath.hashCode() - result = 31 * result + fileData.contentHashCode() - return result - } -} - -fun DeletedFileDetails.toDeletedFile(): DeletedFile = DeletedFile( - id = id, - fileName = fileName, - filePath = filePath, - fileData = fileData -) - -fun DeletedFile.toDeletedFileDetails(): DeletedFileDetails{ - - return DeletedFileDetails( - id = id, - fileName = fileName, - filePath = filePath, - fileData = fileData - ) -} - -fun DeletedFileDetails.restore(deletedFile: DeletedFile): Boolean { - val file = File(filePath) - - if (file.exists()){ - return false - } - return try { - file.writeBytes(deletedFile.fileData) - true - } catch (e: Exception) { - false - } -} -fun DeletedFile.toDeletedFileUiState(): DeletedFileUiState = DeletedFileUiState( - deletedFileDetails = this.toDeletedFileDetails() -) - -fun File.toDeletedFileDetails(): DeletedFileDetails = DeletedFileDetails( - fileName = name, - filePath = path, - fileData = readBytes() -) \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedIFilesViewModel.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedIFilesViewModel.kt deleted file mode 100644 index 2499c194..00000000 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/DeletedIFilesViewModel.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - DeletedIFilesViewModel.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.compose.feature.presentation.deletedfiles - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.etb.filemanager.data.deletedfiles.DeletedFile -import com.etb.filemanager.data.deletedfiles.DeletedFilesRepository -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -class DeletedIFilesViewModel( - deletedFilesRepository: DeletedFilesRepository -): ViewModel() { - - val deletedFilesListUiState: StateFlow = - deletedFilesRepository.getAllDeletedFilesStream().map { DeletedFilesListUiState(it) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS), - initialValue = DeletedFilesListUiState() - ) - companion object { - private const val TIMEOUT_MILLIS = 5_000L - } - -} - -data class DeletedFilesListUiState(val deletedFilesList: List = listOf()) \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/deletedfileslist/DeletedFileListScreen.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/deletedfileslist/DeletedFileListScreen.kt deleted file mode 100644 index 8a048640..00000000 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/deletedfileslist/DeletedFileListScreen.kt +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - DeletedFileListScreen.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.compose.feature.presentation.deletedfiles.deletedfileslist - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.spring -import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.shrinkHorizontally -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Shapes -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import com.etb.filemanager.R -import com.etb.filemanager.compose.feature.presentation.deletedfiles.DeletedFileDetailsViewModel -import com.etb.filemanager.compose.feature.presentation.deletedfiles.DeletedFileEntryViewModel -import com.etb.filemanager.compose.feature.presentation.deletedfiles.DeletedIFilesViewModel -import com.etb.filemanager.compose.feature.presentation.deletedfiles.deletedfileslist.components.BottomSheetInfo -import com.etb.filemanager.compose.feature.presentation.deletedfiles.toDeletedFileDetails -import com.etb.filemanager.compose.feature.provider.AppViewModelProvider -import com.etb.filemanager.compose.feature.provider.BaseScreen -import com.etb.filemanager.data.deletedfiles.DeletedFile -import com.etb.filemanager.manager.adapter.FileModel -import com.etb.filemanager.ui.theme.FileManagerTheme -import com.etb.filemanager.ui.theme.Shapes -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.io.File - - -@OptIn(ExperimentalMaterial3Api::class) -class DeletedFileListScreen : BaseScreen() { - private val TAG = "FileListScreen" - - private var fileModel: FileModel? = null - private var pathList: List? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val bundle = intent.extras - -/* - if (bundle != null) { - fileModel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra("fileModel", FileModel::class.java) - } else { - intent.getParcelableExtra("fileModel") - } - } -*/ - if (bundle != null) { - pathList = bundle.getStringArrayList("pathList")?.toList() - } - - setContent { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - - FileManagerTheme { - Scaffold(topBar = { AppTopBar(scrollBehavior) }, - modifier = Modifier.fillMaxSize(), - content = { paddingValues -> - DeletedFileListBody(paddingValues = paddingValues) - - }) - } - } - } - - @Composable - private fun DeletedFileListBody(paddingValues: PaddingValues) { - val viewModel: DeletedIFilesViewModel = viewModel( - factory = AppViewModelProvider.Factory - ) - - val deletedFileUiState by viewModel.deletedFilesListUiState.collectAsState() - val deletedFileList = deletedFileUiState.deletedFilesList - - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(top = 8.dp) - ) { - if (deletedFileList.isEmpty()) { - Text( - text = stringResource(id = R.string.trash_is_empty), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge - ) - - } else { - DeletedFileList( - innerPadding = paddingValues, deletedFileList = deletedFileList - ) - } - - if (!pathList.isNullOrEmpty()) { - SaveDeletedFilesToDatabase(pathList = pathList!!) - } - - } - } - - - @Composable - fun DeletedFileList(innerPadding: PaddingValues, deletedFileList: List) { - LazyColumn( - contentPadding = innerPadding, - verticalArrangement = Arrangement.spacedBy(8.dp), - - ) { - items(items = deletedFileList, key = { it.id }) { file -> - FileItem( - file = file - ) - } - - } - } - - - @Composable - fun FileItem(file: DeletedFile) { - val image: Painter = painterResource(id = R.drawable.ic_folder) - val resolvedColor = MaterialTheme.colorScheme.onSecondary - val colorOnSecondary = Color(resolvedColor.toArgb()) - var expanded by remember { mutableStateOf(false) } - var deleted by remember { mutableStateOf(false) } - - val extraHeight by animateDpAsState( - targetValue = if (expanded) 100.dp else 75.dp, animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow - ), label = "" - ) - - AnimatedVisibility( - visible = !deleted, enter = expandHorizontally(), exit = shrinkHorizontally() - ) { - Card( - modifier = Modifier - .fillMaxWidth() - .height(extraHeight) - .padding(start = 8.dp, end = 8.dp) - - ) { - Row(modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .background(colorOnSecondary, shape = Shapes().medium) - .clickable { expanded = !expanded } - .padding(8.dp)) { - - Box( - modifier = Modifier - .background( - MaterialTheme.colorScheme.primary, shape = Shapes().medium - ) - .size(width = 55.dp, height = 55.dp) - ) { - Image( - painter = image, - contentDescription = null, - modifier = Modifier - .size(width = 30.dp, height = 30.dp) - .fillMaxWidth() - .align(Alignment.Center), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.inversePrimary) - ) - - } - Column( - modifier = Modifier.padding(start = 8.dp, top = 8.dp) - ) { - Text(text = file.fileName) - AnimatedVisibility(visible = expanded) { - Text( - text = file.filePath, - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - - } - } - AnimatedVisibility( - visible = expanded && !deleted, - ) { - DeletedFileOptionsRow(modifier = Modifier.background( - colorOnSecondary, shape = Shapes().medium - ), file = file, isDeleted = { isDeleted -> deleted = isDeleted }) - } - - } - - @Composable - fun SaveDeletedFilesToDatabase(pathList: List) { - - pathList.forEach { path -> - val file = File(path) - SaveDeletedFileToDatabase(file = file) - } - } - - - @Composable - fun SaveDeletedFileToDatabase(file: File) { - val viewModel: DeletedFileEntryViewModel = viewModel(factory = AppViewModelProvider.Factory) - - //2 = 2MB - val recommendedSize = file.length() < 2 * 1024 * 1024 - if (recommendedSize) { - val deletedFileDetails = file.toDeletedFileDetails() - - viewModel.updateUiState(deletedFileDetails) - - LaunchedEffect(viewModel) { - viewModel.saveDeletedFile(deletedFileDetails = deletedFileDetails) - } - } - } - /* - @Composable - fun SaveDeletedFileToDatabase(file: File) { - val viewModel: DeletedFileEntryViewModel = viewModel(factory = AppViewModelProvider.Factory) - - //2 = 2MB - val recommendedSize = file.length() < 2 * 1024 * 1024 - if (recommendedSize) { - val updatedDetails = viewModel.deletedFileUIState.deletedFileDetails.copy( - fileName = file.name, filePath = file.path, fileData = file.readBytes() - ) - - viewModel.updateUiState(updatedDetails) - - LaunchedEffect(viewModel) { - viewModel.saveDeletedFile() - } - } - } - */ - - @Composable - fun RemoveDeletedFileToDatabase(file: DeletedFile) { - val viewModel: DeletedFileDetailsViewModel = - viewModel(factory = AppViewModelProvider.Factory) - val coroutineScope = rememberCoroutineScope() - - - LaunchedEffect(viewModel) { - coroutineScope.launch { - viewModel.deleteFileFromDatabase(file) - } - - } - - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun AppTopBar(scrollBehavior: TopAppBarScrollBehavior) { - LargeTopAppBar( - title = { - Text( - text = stringResource(id = R.string.experimental_trash_can), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - scrollBehavior = scrollBehavior - ) - } - - - @Composable - fun DeletedFileOptionsRow( - modifier: Modifier = Modifier, - file: DeletedFile, - isDeleted: (Boolean) -> Unit, - viewModel: DeletedFileDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory) - ) { - - val coroutineScope = rememberCoroutineScope() - var deleted by remember { mutableStateOf(false) } - val showBottomSheet = remember { mutableStateOf(false) } - - - LaunchedEffect(deleted) { - if (deleted) { - isDeleted(true) - viewModel.viewModelScope.launch { - delay(3000L) - viewModel.deleteFileFromDatabase(deletedFile = file) - } - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(Shapes.large), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - - ) { - - DeletedFileOption( - title = stringResource(id = R.string.restore), modifier = modifier.weight(1f) - ) { - - coroutineScope.launch() { - val result = viewModel.restoreDeletedFile(deletedFile = file) - launch(Dispatchers.Main) { - deleted = result - } - } - } - DeletedFileOption( - title = stringResource(id = R.string.delete), modifier = modifier.weight(1f) - ) { - // deleted = true - showBottomSheet.value = true - } - - if (showBottomSheet.value) { - BottomSheetInfo() - } - - } - } - - - @Composable - fun DeletedFileOption( - title: String, modifier: Modifier = Modifier, - onClick: () -> Unit, - ) { - Card( - modifier = modifier - .fillMaxWidth() - .height(60.dp) - ) { - Row(modifier = modifier - .fillMaxWidth() - .height(60.dp) - .clickable { onClick() } - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically) { - Text( - text = title, - fontSize = 16.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = modifier.fillMaxWidth() - - ) - - } - - } - } -} - diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/deletedfileslist/components/BottomSheetInfo.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/deletedfileslist/components/BottomSheetInfo.kt deleted file mode 100644 index a4bfa8fa..00000000 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/deletedfiles/deletedfileslist/components/BottomSheetInfo.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - BottomSheetInfo.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.compose.feature.presentation.deletedfiles.deletedfileslist.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text -import androidx.compose.material3.surfaceColorAtElevation -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.etb.filemanager.R -import com.etb.filemanager.ui.theme.AppShapes -import com.etb.filemanager.ui.theme.Shapes - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BottomSheetInfo() { - ModalBottomSheet( - modifier = Modifier - .padding(horizontal = 16.dp), - onDismissRequest = { - }, - shape = Shapes.extraSmall, - containerColor = Color.Transparent, - contentColor = Color.White, - dragHandle = null - ) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - BottomSheetContainer { - - } - } - Spacer(modifier = Modifier.size(16.dp)) - BottomSheetButtonsRow() - Spacer(modifier = Modifier.size(16.dp)) - - } -} - - - -@Composable -private fun BottomSheetContainer( - content: @Composable () -> Unit -) { - - - Column( - modifier = Modifier - .fillMaxWidth() - .clip(Shapes.medium) - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(4.dp)) - ) { - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) - .padding(16.dp) - ) { - Row( - modifier = Modifier - .padding(bottom = 16.dp), - verticalAlignment = Alignment.Top - ) { - Column( - modifier = Modifier - .weight(1f) - ) { - Text( - text = "FileManagerSphere", - fontSize = 20.sp, - fontWeight = FontWeight.Light, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - } - content() - } -} - -@Composable -fun BottomSheetButtonsRow(){ - val resolvedColor = MaterialTheme.colorScheme.onSecondary - val colorOnSecondary = Color(resolvedColor.toArgb()) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(Shapes.large), - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically, - - ) { - BottomSheetButton( - text = stringResource(id = R.string.ok), - modifier = Modifier - .weight(1f) - .background(color = colorOnSecondary, shape = AppShapes.cutLeftMedium)) { - } - BottomSheetButton( - text = stringResource(id = R.string.cancel), - modifier = Modifier - .weight(1f) - .background(color = colorOnSecondary, shape = AppShapes.cutRightMedium)) { - } - } -} - -@Composable -fun BottomSheetButton( - text: String, - modifier: Modifier = Modifier, - onClick: () -> Unit -){ - Card( - modifier = modifier - .fillMaxWidth() - .height(60.dp) - ) { - Row(modifier = modifier - .fillMaxWidth() - .height(60.dp) - .clickable { onClick() } - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically) { - Text( - text = text, - fontSize = 16.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = modifier.fillMaxWidth() - - ) - - } - - } - -} - - - diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/recentfiles/components/RecentFilesWidget.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/recentfiles/components/RecentFilesWidget.kt deleted file mode 100644 index 728f0c28..00000000 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/recentfiles/components/RecentFilesWidget.kt +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - RecentFilesWidget.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.compose.feature.presentation.recentfiles.components - -import android.content.Intent -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -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.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Add -import androidx.compose.material.icons.outlined.PhoneAndroid -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material3.Card -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.FilterQuality -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.compose.rememberAsyncImagePainter -import coil.request.ImageRequest -import com.etb.filemanager.R -import com.etb.filemanager.activity.SettingsActivity -import com.etb.filemanager.compose.core.presentation.components.CircularProgressBar -import com.etb.filemanager.compose.core.presentation.components.ProgressBar -import com.etb.filemanager.compose.feature.presentation.recentimages.model.RecentImage -import com.etb.filemanager.compose.feature.presentation.storage.model.StorageItem -import com.etb.filemanager.files.util.fetchRecentImagesUris -import com.etb.filemanager.manager.category.adapter.CategoryFileModel -import com.etb.filemanager.manager.category.adapter.getCategories -import com.etb.filemanager.settings.preference.Preferences -import com.etb.filemanager.ui.theme.AppShapes -import com.etb.filemanager.ui.theme.FileManagerTheme -import com.etb.filemanager.ui.theme.Shapes -import java.nio.file.Paths -import kotlin.io.path.pathString - - -@Composable -fun RecentFilesWidget() { - FileManagerTheme { - Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 80.dp) - ) { - MobileInfoWidget() - Spacer(modifier = Modifier.size(16.dp)) - - StorageListWidget() - Spacer(modifier = Modifier.size(16.dp)) - - CategoryFilesWidget() - Spacer(modifier = Modifier.size(16.dp)) - RecentImagesWidget() - - } - - } -} - -@Composable -fun CategoryFilesWidget() { - CardWidget( - modifier = Modifier - .height(230.dp) - ) { - Column( - modifier = Modifier - .padding(top = 16.dp) - - ) { - CategoryFilesList(categoryFilesList = getCategories(LocalContext.current)) - Row( - horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth() - ) { - - IconButton(onClick = { /*TODO*/ }) { - Icon( - Icons.Outlined.Add, - contentDescription = stringResource(id = R.string.category_add), - tint = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier - .size(30.dp) - ) - } - - } - - } - - - - } - - -} - -@Composable -fun MobileInfoWidget() { - FileManagerTheme { - - CardWidget(modifier = Modifier.height(199.dp)) { - InfoStorage() - } - } -} - -@Composable -fun StorageListWidget() { - - - val storageList = mutableListOf() - storageList.add( - StorageItem( - Paths.get( - Preferences.Behavior.defaultFolder, - stringResource(id = R.string.internal_st) - ) - ) - ) - CardWidget( - modifier = Modifier.height(85.dp), - ) { - StorageList(storageItemList = storageList) - } -} - - -@Composable -fun InfoStorage() { - val context = LocalContext.current - - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.size(52.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - ) { - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - - ) { - CardWithText(text = stringResource(id = R.string.used_space)) - Text(text = "85 GB", - color = MaterialTheme.colorScheme.inverseOnSurface) - } - - Spacer(modifier = Modifier.size(32.dp)) - Column( - modifier = Modifier - .padding(bottom = 15.dp) - ){ - CircularProgressBar( - progress = 80.0f, - text = "108", - subText = stringResource(id = R.string.gb), - strokeWidth = 10.dp, - modifier = Modifier - .size(95.dp) - ) - - } - - Spacer(modifier = Modifier.size(32.dp)) - - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CardWithText(text = stringResource(id = R.string.free_space)) - Text(text = "25 GB", color = MaterialTheme.colorScheme.inverseOnSurface) - } - - - } - Row( - horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth() - ) { - - IconButton( - onClick = { - val settingsIntent: Intent = SettingsActivity().getIntent(context) - context.startActivity(settingsIntent) - }) { - Icon( - Icons.Outlined.Settings, - contentDescription = "Settings", - tint = MaterialTheme.colorScheme.onPrimary, - ) - } - - } - - - } -} - -@Composable -private fun CardWithText(text: String) { - Card( - modifier = Modifier.size(width = 80.dp, height = 24.dp) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background( - color = MaterialTheme.colorScheme.onPrimary - ) - ) { - Text( - text = text, - fontSize = 10.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.inverseSurface, - modifier = Modifier.align(alignment = Alignment.Center) - - ) - } - } - -} - -@Composable -fun CategoryFilesList(categoryFilesList: List) { - LazyVerticalGrid( - columns = GridCells.Fixed(4), verticalArrangement = Arrangement.spacedBy(5.dp) - ) { - items(categoryFilesList) { category -> - CategoryItem(category = category) - } - - } -} - -@Composable -fun CategoryItem(category: CategoryFileModel) { - val imageResId = category.icon - - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Card( - modifier = Modifier - .size(width = 62.dp, 62.dp) - .clip(AppShapes.rounded.Medium) - - - ) { - Box( - modifier = Modifier - .size(width = 62.dp, 62.dp) - .background( - color = MaterialTheme.colorScheme.onPrimary - ) - ) { - Image( - painter = painterResource(id = imageResId), - contentDescription = "FileItemImage", - modifier = Modifier - .size(width = 28.dp, height = 28.dp) - .align(alignment = Alignment.Center), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary) - ) - } - } - - Spacer(modifier = Modifier.height(2.dp)) - - Text( - text = category.title, - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.inverseOnSurface - - ) - - } -} - - -@Composable -fun StorageList(storageItemList: List) { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(4.dp), - - ) { - items(items = storageItemList, key = { it.path.pathString }) { storage -> - ProcessStorageItem(storage = storage) - } - - } -} - -@Composable -fun ProcessStorageItem(storage: StorageItem) { - - Column( - modifier = Modifier.padding(8.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(0.dp) - ) { - Icon( - imageVector = Icons.Outlined.PhoneAndroid, - contentDescription = storage.name, - tint = MaterialTheme.colorScheme.inverseOnSurface, - modifier = Modifier.size(17.dp) - ) - Text( - text = storage.name, - color = MaterialTheme.colorScheme.inverseOnSurface, - fontSize = 12.sp - ) - } - Spacer(modifier = Modifier.size(4.dp)) - - ProgressBar(progress = 70.0f) - Spacer(modifier = Modifier.size(4.dp)) - - Row { - Row( - verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f) - - ) { - Text( - text = "Free Space: 25 GB of 128", - color = MaterialTheme.colorScheme.inverseOnSurface, - fontSize = 11.sp - ) - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - modifier = Modifier.padding(0.dp) - - ) { - Text( - text = "Explore", - color = MaterialTheme.colorScheme.inverseOnSurface, - fontSize = 11.sp - ) - } - - - } - } -} - -@Composable -fun RecentImagesWidget() { - val recentImagesUriList = remember { mutableStateListOf() } - val context = LocalContext.current - - LaunchedEffect(Unit) { - val images = fetchRecentImagesUris(context) - recentImagesUriList.addAll(images) - } - - CardWidget( - ) { - RecentImagesList(recentImagesList = recentImagesUriList) - } -} - - -@Composable -fun RecentImagesList(recentImagesList: MutableList) { - LazyVerticalGrid( - columns = GridCells.Fixed(3), - verticalArrangement = Arrangement.spacedBy(10.dp), - horizontalArrangement = Arrangement.spacedBy(5.dp), - contentPadding = PaddingValues(8.dp) - ) { - items(recentImagesList, key = { it.imageUri.toString() }) { image -> - RecentImageItem(image = image) - } - - } - -} - -@Composable -fun RecentImageItem(image: RecentImage, modifier: Modifier = Modifier) { - val label = image.imageUri.toString().substringAfterLast("/") - - val key = "image_${label}" - val painter = rememberAsyncImagePainter( - model = ImageRequest.Builder(LocalContext.current) - .data(image.imageUri) - .memoryCacheKey(key) - .diskCacheKey(key) - .build(), - contentScale = ContentScale.Crop, - filterQuality = FilterQuality.None, - ) - - Image( - modifier = modifier - .size(width = 110.dp, height = 101.dp) - .clip(Shapes.medium), - painter = painter, - contentScale = ContentScale.Crop, - contentDescription = null - ) - - -} - -@Composable -fun CardWidget( - modifier: Modifier = Modifier, - columModifier: Modifier = Modifier, - content: @Composable () -> Unit -) { - - Card( - modifier = modifier - .fillMaxWidth() - .height(220.dp) - .clip(AppShapes.shapeFromPreferences) - - ) { - Column( - modifier = columModifier - .fillMaxWidth() - .fillMaxHeight() - .clip(AppShapes.shapeFromPreferences) - - .background( - color = MaterialTheme.colorScheme.primary - ) - ) { - content() - - } - } -} - -@Preview -@Composable -fun PreviewCategoryList() { - RecentFilesWidget() -} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/storage/model/StorageItem.kt b/app/src/main/java/com/etb/filemanager/compose/feature/presentation/storage/model/StorageItem.kt deleted file mode 100644 index 83453c61..00000000 --- a/app/src/main/java/com/etb/filemanager/compose/feature/presentation/storage/model/StorageItem.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - StorageItem.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.compose.feature.presentation.storage.model - -import java.nio.file.Path -import kotlin.io.path.name - -data class StorageItem(val path: Path, val name: String = path.name) diff --git a/app/src/main/java/com/etb/filemanager/compose/feature/provider/AppViewModelProvider.kt b/app/src/main/java/com/etb/filemanager/compose/feature/provider/AppViewModelProvider.kt deleted file mode 100644 index 6f600a3e..00000000 --- a/app/src/main/java/com/etb/filemanager/compose/feature/provider/AppViewModelProvider.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - AppViewModelProvider.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.compose.feature.provider - -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory -import com.etb.filemanager.compose.feature.presentation.deletedfiles.DeletedFileDetailsViewModel -import com.etb.filemanager.compose.feature.presentation.deletedfiles.DeletedFileEntryViewModel -import com.etb.filemanager.compose.feature.presentation.deletedfiles.DeletedIFilesViewModel -import com.etb.filemanager.data.SphereApplication - -object AppViewModelProvider { - val Factory = viewModelFactory { - initializer { - DeletedFileEntryViewModel(SphereApplication().container.deletedFileRepository) - - } - initializer { - DeletedIFilesViewModel(SphereApplication().container.deletedFileRepository) - } - initializer { - DeletedFileDetailsViewModel( - SphereApplication().container.deletedFileRepository - ) - } - - } -} -fun CreationExtras.SphereApplication(): SphereApplication = - (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as SphereApplication) \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/compose/mapper/ChatMapper.kt b/app/src/main/java/com/etb/filemanager/compose/mapper/ChatMapper.kt new file mode 100644 index 00000000..d52ef7b0 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/compose/mapper/ChatMapper.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatMapper.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.compose.mapper + +import com.etb.filemanager.compose.core.models.Chat +import com.etb.filemanager.data.entities.ChatEntity + + +fun ChatEntity.toChat() = Chat( + id = id, + chatSettings = chatSettings, + messages = messages +) +fun Chat.toChatEntity() = ChatEntity( + id = id, + chatSettings = chatSettings, + messages = messages +) \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/SphereApplication.kt b/app/src/main/java/com/etb/filemanager/data/SphereApplication.kt index 4976d5f0..e5546707 100644 --- a/app/src/main/java/com/etb/filemanager/data/SphereApplication.kt +++ b/app/src/main/java/com/etb/filemanager/data/SphereApplication.kt @@ -8,17 +8,7 @@ package com.etb.filemanager.data import android.app.Application -import com.etb.filemanager.data.deletedfiles.AppContainer -import com.etb.filemanager.data.deletedfiles.AppDataContainer import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class SphereApplication : Application() { - - lateinit var container: AppContainer - - override fun onCreate() { - super.onCreate() - container = AppDataContainer(this) - } -} \ No newline at end of file +class SphereApplication : Application() \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/converters/ChatSettingConverter.kt b/app/src/main/java/com/etb/filemanager/data/converters/ChatSettingConverter.kt new file mode 100644 index 00000000..99832dd8 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/converters/ChatSettingConverter.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatSettingConverter.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.converters + +import androidx.room.TypeConverter +import com.etb.filemanager.compose.core.models.ChatSettings +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class ChatSettingsConverter { + @TypeConverter + fun fromChatSettings(chatSettings: ChatSettings): String { + return Json.encodeToString(chatSettings) + } + + @TypeConverter + fun toChatSettings(chatSettings: String): ChatSettings { + return Json.decodeFromString(chatSettings) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/converters/MessagesConverter.kt b/app/src/main/java/com/etb/filemanager/data/converters/MessagesConverter.kt new file mode 100644 index 00000000..d60d9dfc --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/converters/MessagesConverter.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - MessagesConverter.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.converters + +import androidx.room.TypeConverter +import com.etb.filemanager.compose.core.models.Message +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class MessagesConverter { + @TypeConverter + fun fromMessages(messages: List): String { + return Json.encodeToString(messages) + } + @TypeConverter + fun toMessages(messages: String): List { + return Json.decodeFromString(messages) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/datasource/AppDatabase.kt b/app/src/main/java/com/etb/filemanager/data/datasource/AppDatabase.kt new file mode 100644 index 00000000..cbd1f1e6 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/datasource/AppDatabase.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - AppDatabase.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.datasource + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.etb.filemanager.data.converters.ChatSettingsConverter +import com.etb.filemanager.data.converters.MessagesConverter +import com.etb.filemanager.data.entities.ChatEntity + +@Database(entities = [ChatEntity::class], version = 1, exportSchema = true) +@TypeConverters(ChatSettingsConverter::class, MessagesConverter::class) +abstract class AppDatabase: RoomDatabase() { + abstract fun chatDao(): ChatDao +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/datasource/AppMigrations.kt b/app/src/main/java/com/etb/filemanager/data/datasource/AppMigrations.kt new file mode 100644 index 00000000..c6508bbc --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/datasource/AppMigrations.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - AppMigrations.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.datasource + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +object AppMigrations { + + val MIGRATION_1_2: Migration = object : Migration(1, 2){ + override fun migrate(db: SupportSQLiteDatabase) { + //No need to do anything, this is an empty migration for now + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/datasource/ChatDao.kt b/app/src/main/java/com/etb/filemanager/data/datasource/ChatDao.kt new file mode 100644 index 00000000..86288cb3 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/datasource/ChatDao.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatDao.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.datasource + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.Upsert +import com.etb.filemanager.data.entities.ChatEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ChatDao { + @Upsert + suspend fun upsert(chatEntity: ChatEntity) + @Query("SELECT * FROM ChatEntity") + fun getAllChat(): Flow> + @Delete + suspend fun delete(chatEntity: ChatEntity) +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/deletedfiles/AppContainer.kt b/app/src/main/java/com/etb/filemanager/data/deletedfiles/AppContainer.kt deleted file mode 100644 index 1244d4af..00000000 --- a/app/src/main/java/com/etb/filemanager/data/deletedfiles/AppContainer.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - AppContainer.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.data.deletedfiles - -import android.content.Context - -interface AppContainer { - val deletedFileRepository: DeletedFilesRepository -} - -class AppDataContainer(private val context: Context) : AppContainer{ - override val deletedFileRepository: DeletedFilesRepository by lazy { - OfflineDeletedFilesRepository(AppDatabase.getDatabase(context).deletedFileDao()) - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/deletedfiles/AppDatabase.kt b/app/src/main/java/com/etb/filemanager/data/deletedfiles/AppDatabase.kt deleted file mode 100644 index 6da49321..00000000 --- a/app/src/main/java/com/etb/filemanager/data/deletedfiles/AppDatabase.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - AppDatabase.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.data.deletedfiles - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase - -@Database(entities = [DeletedFile::class], version =1, exportSchema = false) -abstract class AppDatabase : RoomDatabase() { - - abstract fun deletedFileDao(): DeletedFileDao - - companion object { - - @Volatile - private var Instance: AppDatabase? = null - - fun getDatabase(context: Context): AppDatabase { - return Instance ?: synchronized(this){ - Room.databaseBuilder(context, AppDatabase::class.java, "app_database") - .fallbackToDestructiveMigration() - .build() - .also { Instance = it } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFile.kt b/app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFile.kt deleted file mode 100644 index 8c32ad21..00000000 --- a/app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFile.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - DeletedFile.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.data.deletedfiles - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "deleted_files") -data class DeletedFile( - @PrimaryKey(autoGenerate = true) - val id: Int = 0, - val fileName: String, - val filePath: String, - val fileData: ByteArray -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as DeletedFile - - if (id != other.id) return false - if (fileName != other.fileName) return false - if (filePath != other.filePath) return false - if (!fileData.contentEquals(other.fileData)) return false - - return true - } - - override fun hashCode(): Int { - var result = id - result = 31 * result + fileName.hashCode() - result = 31 * result + filePath.hashCode() - result = 31 * result + fileData.contentHashCode() - return result - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFileDao.kt b/app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFileDao.kt deleted file mode 100644 index b57ea5ca..00000000 --- a/app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFileDao.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - DeletedFileDao.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.data.deletedfiles - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Update -import com.etb.filemanager.manager.adapter.FileModel -import kotlinx.coroutines.flow.Flow - -@Dao -interface DeletedFileDao { - @Query("SELECT * from deleted_files ORDER BY fileName ASC") - fun getAllDeletedFiles(): Flow> - - @Query("SELECT * from deleted_files WHERE id = :id") - fun getDeletedFile(id: Int): Flow - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insert(deletedFile: DeletedFile) - - @Update - suspend fun update(deletedFile: DeletedFile) - - @Delete - suspend fun delete(deletedFile: DeletedFile) -} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFilesRepository.kt b/app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFilesRepository.kt deleted file mode 100644 index e1169a3e..00000000 --- a/app/src/main/java/com/etb/filemanager/data/deletedfiles/DeletedFilesRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - DeletedFilesRepository.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.data.deletedfiles - -import kotlinx.coroutines.flow.Flow - -interface DeletedFilesRepository { - - fun getAllDeletedFilesStream(): Flow> - - fun getDeletedFileStream(id: Int): Flow - - suspend fun insertDeletedFile(deletedFile: DeletedFile) - - suspend fun deleteFileFromDatabase(deletedFile: DeletedFile) - - suspend fun updateDeletedFile(deletedFile: DeletedFile) -} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/deletedfiles/OfflineDeletedFilesRepository.kt b/app/src/main/java/com/etb/filemanager/data/deletedfiles/OfflineDeletedFilesRepository.kt deleted file mode 100644 index 3d1a294c..00000000 --- a/app/src/main/java/com/etb/filemanager/data/deletedfiles/OfflineDeletedFilesRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2023 Juan Nascimento - * Part of FileManagerSphere - OfflineDeletedFilesRepository.kt - * SPDX-License-Identifier: GPL-3.0-or-later - * More details at: https://www.gnu.org/licenses/ - */ - -package com.etb.filemanager.data.deletedfiles - -import kotlinx.coroutines.flow.Flow - -class OfflineDeletedFilesRepository(private val deletedFileDao: DeletedFileDao) : DeletedFilesRepository { - override fun getAllDeletedFilesStream(): Flow> = deletedFileDao.getAllDeletedFiles() - override fun getDeletedFileStream(id: Int): Flow = deletedFileDao.getDeletedFile(id) - override suspend fun insertDeletedFile(deletedFile: DeletedFile) = deletedFileDao.insert(deletedFile) - override suspend fun deleteFileFromDatabase(deletedFile: DeletedFile) = deletedFileDao.delete(deletedFile) - override suspend fun updateDeletedFile(deletedFile: DeletedFile) = deletedFileDao.update(deletedFile) -} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/di/AppModule.kt b/app/src/main/java/com/etb/filemanager/data/di/AppModule.kt new file mode 100644 index 00000000..e182564d --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/di/AppModule.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - AppModule.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.di + +import android.content.Context +import androidx.room.Room +import com.etb.filemanager.BuildConfig +import com.etb.filemanager.compose.core.presentation.util.Prompt +import com.etb.filemanager.data.datasource.AppDatabase +import com.etb.filemanager.data.datasource.AppMigrations +import com.etb.filemanager.data.datasource.ChatDao +import com.etb.filemanager.data.repository.ChatRepository +import com.etb.filemanager.data.repository.ChatRepositoryImpl +import com.google.ai.client.generativeai.Chat +import com.google.ai.client.generativeai.GenerativeModel +import com.google.ai.client.generativeai.type.content +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase { + return Room.databaseBuilder( + context, AppDatabase::class.java, "app_database" + ).addMigrations(AppMigrations.MIGRATION_1_2).build() + } + + @Provides + @Singleton + fun provideChatDao(appDatabase: AppDatabase): ChatDao { + return appDatabase.chatDao() + } + + @Provides + @Singleton + fun provideChatRepository(chatDao: ChatDao): ChatRepository { + return ChatRepositoryImpl(chatDao) + } + + @Provides + @Singleton + fun provideGenerativeModel(): GenerativeModel { + return GenerativeModel( + modelName = "gemini-pro", apiKey = BuildConfig.apiKey + ) + } + + @Provides + @Singleton + fun provideChat(generativeModel: GenerativeModel): Chat { + val chat = generativeModel.startChat( + history = listOf(content( + role = "user" + ) { + text(Prompt.FILE_OPERATIONS) + }, content( + role = "model" + ) { + text(Prompt.FILE_OPERATIONS_MODEL) + }, content( + role = "user" + ) { + text(Prompt.FILE_OPERATIONS_EXAMPLE) + }, content( + role = "model", + ) { + text(Prompt.FILE_OPERATIONS_EXAMPLE_MODEL) + }) + ) + + return chat + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/entities/ChatEntity.kt b/app/src/main/java/com/etb/filemanager/data/entities/ChatEntity.kt new file mode 100644 index 00000000..93c8ef61 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/entities/ChatEntity.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatEntity.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.etb.filemanager.compose.core.models.ChatSettings +import com.etb.filemanager.compose.core.models.Message +import com.etb.filemanager.data.converters.ChatSettingsConverter +import com.etb.filemanager.data.converters.MessagesConverter + +@Entity +data class ChatEntity( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + @TypeConverters(ChatSettingsConverter::class) + val chatSettings: ChatSettings = ChatSettings(), + @TypeConverters(MessagesConverter::class) + val messages: List = emptyList() +) diff --git a/app/src/main/java/com/etb/filemanager/data/repository/ChatRepository.kt b/app/src/main/java/com/etb/filemanager/data/repository/ChatRepository.kt new file mode 100644 index 00000000..dc7f2f8f --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/repository/ChatRepository.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatRepository.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.repository + +import com.etb.filemanager.data.entities.ChatEntity +import kotlinx.coroutines.flow.Flow + +interface ChatRepository { + + suspend fun upsert(chat: ChatEntity) + + suspend fun getAllChat(): Flow> + + suspend fun delete(chat: ChatEntity) +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/repository/ChatRepositoryImpl.kt b/app/src/main/java/com/etb/filemanager/data/repository/ChatRepositoryImpl.kt new file mode 100644 index 00000000..c7b84dbf --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/repository/ChatRepositoryImpl.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - ChatRepositoryImpl.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.repository + +import com.etb.filemanager.data.datasource.ChatDao +import com.etb.filemanager.data.entities.ChatEntity +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class ChatRepositoryImpl @Inject constructor( + private val chatDao: ChatDao +): ChatRepository { + + override suspend fun upsert(chat: ChatEntity) = chatDao.upsert(chat) + override suspend fun getAllChat(): Flow> = chatDao.getAllChat() + + override suspend fun delete(chat: ChatEntity) = chatDao.delete(chat) +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/data/repository/GenerativeModelManager.kt b/app/src/main/java/com/etb/filemanager/data/repository/GenerativeModelManager.kt new file mode 100644 index 00000000..ff9e6778 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/data/repository/GenerativeModelManager.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - GenerativeModelManager.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.data.repository + +import com.etb.filemanager.BuildConfig +import com.etb.filemanager.compose.core.models.FileOperationResult +import com.etb.filemanager.compose.core.models.Message +import com.etb.filemanager.compose.core.models.Participant +import com.etb.filemanager.compose.core.presentation.util.FileOperationCallback +import com.etb.filemanager.compose.core.presentation.util.FileOperations +import com.etb.filemanager.compose.core.presentation.util.Prompt +import com.google.ai.client.generativeai.GenerativeModel +import com.google.ai.client.generativeai.type.Content +import com.google.ai.client.generativeai.type.InvalidStateException +import com.jn.airesponsematcher.extensions.process +import com.jn.airesponsematcher.processor.Output +import kotlinx.coroutines.CoroutineScope +import javax.inject.Inject + +class GenerativeModelManager @Inject constructor( +) { + + private val generativeModel = GenerativeModel( + modelName = "gemini-pro", apiKey = BuildConfig.apiKey + ) + + val chat = generativeModel.startChat( + history = Prompt.chatHistory + ) + private val errorText = "An error occurred while generating the message." + + suspend fun generateContent( + message: String, + chatHistory: List, + scope: CoroutineScope + ): Message { + + chat.history.addAll(chatHistory) + + return try { + val response = chat.sendMessage(message) + processResponse(response.text, scope) + + } catch (e: InvalidStateException) { + handleErrorState(e) + } catch (e: Exception) { + handleGenericError(e) + } + } + + + private fun processResponse(text: String?, scope: CoroutineScope): Message { + val isError = text == null + var textResult = errorText + var participant = Participant.ERROR + + if (!isError) { + textResult = text!! + participant = Participant.MODEL + } + val (operationResults, textProcessed) = processOutput(textResult, scope) + + return Message( + text = textProcessed, participant = participant, operationResults = operationResults + ) + } + + private fun handleErrorState(e: InvalidStateException): Message { + val errorStateText = "Instance has an active request." + return Message( + text = "$errorStateText\n\nError:\n${e.message}", participant = Participant.ERROR + ) + } + + private fun handleGenericError(e: Exception): Message { + return Message( + text = "$errorText\n\nError:\n${e.message}", participant = Participant.ERROR + ) + } + + private fun processOutput(aiOutput: String, scope: CoroutineScope): Pair, String> { + val operationResults = mutableListOf() + val operationCallback = object : FileOperationCallback { + override fun onResolve(operationResult: FileOperationResult) { + operationResults.add(operationResult) + } + + } + val fileOperation = FileOperations(scope, operationCallback) + val output = Output(aiOutput, fileOperation.operations) + return operationResults to output.process().trim() + } + +} diff --git a/app/src/main/java/com/etb/filemanager/files/extensions/ApkExt.kt b/app/src/main/java/com/etb/filemanager/files/extensions/ApkExt.kt index 18edcbc5..daf87d71 100644 --- a/app/src/main/java/com/etb/filemanager/files/extensions/ApkExt.kt +++ b/app/src/main/java/com/etb/filemanager/files/extensions/ApkExt.kt @@ -20,6 +20,7 @@ import android.provider.Settings import androidx.annotation.RequiresApi import androidx.core.content.FileProvider import androidx.core.content.pm.PackageInfoCompat +import com.etb.filemanager.BuildConfig import com.etb.filemanager.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -49,7 +50,7 @@ suspend fun getInstalledApkInfo(context: Context, appFilter: AppFilter): List = withContext apkInfoList.add( AppInfo( - appName = appName, - appIcon = appIcon, - packageName = packageName, - apkPath = path + appName = appName, appIcon = appIcon, packageName = packageName, apkPath = path ) ) } @@ -167,9 +165,7 @@ fun openAppSettings(context: Context, packageName: String) { fun installApk(context: Context, apkFilePath: Path) { val uri = FileProvider.getUriForFile( - context, - context.packageName + ".fileprovider", - apkFilePath.toFile() + context, BuildConfig.FILE_PROVIDER_AUTHORITY, apkFilePath.toFile() ) val installIntent = Intent(Intent.ACTION_VIEW) @@ -180,17 +176,14 @@ fun installApk(context: Context, apkFilePath: Path) { } enum class AppFilter { - ALL, - NON_SYSTEM, - SYSTEM, - UNINSTALLED_INTERNAL -} - -private fun isSystemPackage(applicationInfo: ApplicationInfo): Boolean { - return applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0 + ALL, NON_SYSTEM, SYSTEM, UNINSTALLED_INTERNAL } -fun getAppMetadata(appInfo: AppInfo, context: Context, appIsInstalled: Boolean): MutableList { +fun getAppMetadata( + appInfo: AppInfo, + context: Context, + appIsInstalled: Boolean +): MutableList { val packageManager = context.packageManager val appMetadataList = mutableListOf() val packageInfo = getPackageInfo(appIsInstalled, appInfo, packageManager) @@ -208,12 +201,12 @@ fun getAppMetadata(appInfo: AppInfo, context: Context, appIsInstalled: Boolean): val compileSdkVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { info.compileSdkVersion } else { - "" + "" }.toString() val compileSdkVersionCodename = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { info.compileSdkVersionCodename } else { - "" + "" }.toString() appMetadataList.add( @@ -222,12 +215,42 @@ fun getAppMetadata(appInfo: AppInfo, context: Context, appIsInstalled: Boolean): content = if (isSystemApp) context.getString(R.string.yes) else context.getString(R.string.no) ) ) - appMetadataList.add(AppMetadata(label = context.getString(R.string.version_name_label), content = versionName)) - appMetadataList.add(AppMetadata(label = context.getString(R.string.version_code_label), content = versionCode)) - appMetadataList.add(AppMetadata(label = context.getString(R.string.min_sdk_version_label), content = minSdkVersion)) - appMetadataList.add(AppMetadata(label = context.getString(R.string.target_sdk_version_label), content = targetSdkVersion)) - appMetadataList.add(AppMetadata(label = context.getString(R.string.compile_sdk_version_label), content = compileSdkVersion)) - appMetadataList.add(AppMetadata(label = context.getString(R.string.compile_sdk_version_codename_label), content = compileSdkVersionCodename)) + appMetadataList.add( + AppMetadata( + label = context.getString(R.string.version_name_label), + content = versionName + ) + ) + appMetadataList.add( + AppMetadata( + label = context.getString(R.string.version_code_label), + content = versionCode + ) + ) + appMetadataList.add( + AppMetadata( + label = context.getString(R.string.min_sdk_version_label), + content = minSdkVersion + ) + ) + appMetadataList.add( + AppMetadata( + label = context.getString(R.string.target_sdk_version_label), + content = targetSdkVersion + ) + ) + appMetadataList.add( + AppMetadata( + label = context.getString(R.string.compile_sdk_version_label), + content = compileSdkVersion + ) + ) + appMetadataList.add( + AppMetadata( + label = context.getString(R.string.compile_sdk_version_codename_label), + content = compileSdkVersionCodename + ) + ) } @@ -236,12 +259,8 @@ fun getAppMetadata(appInfo: AppInfo, context: Context, appIsInstalled: Boolean): } - - fun getPackageInfo( - appIsInstalled: Boolean, - appInfo: AppInfo, - packageManager: PackageManager + appIsInstalled: Boolean, appInfo: AppInfo, packageManager: PackageManager ): PackageInfo? { val packageInfo = if (appIsInstalled) { packageManager.getPackageInfo(appInfo.packageName, 0) diff --git a/app/src/main/java/com/etb/filemanager/files/extensions/UriExt.kt b/app/src/main/java/com/etb/filemanager/files/extensions/UriExt.kt new file mode 100644 index 00000000..57c7e831 --- /dev/null +++ b/app/src/main/java/com/etb/filemanager/files/extensions/UriExt.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Juan Nascimento + * Part of FileManagerSphere - UriExt.kt + * SPDX-License-Identifier: GPL-3.0-or-later + * More details at: https://www.gnu.org/licenses/ + */ + +package com.etb.filemanager.files.extensions + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri + +fun Uri.toBitmap(context: Context): Bitmap? { + return try { + val inputStream = context.contentResolver.openInputStream(this) + BitmapFactory.decodeStream(inputStream) + } catch (e: Exception){ + e.printStackTrace() + null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etb/filemanager/files/provider/archive/common/properties/BasicPropertiesFragment.kt b/app/src/main/java/com/etb/filemanager/files/provider/archive/common/properties/BasicPropertiesFragment.kt index fa959175..8aedc21c 100644 --- a/app/src/main/java/com/etb/filemanager/files/provider/archive/common/properties/BasicPropertiesFragment.kt +++ b/app/src/main/java/com/etb/filemanager/files/provider/archive/common/properties/BasicPropertiesFragment.kt @@ -9,6 +9,7 @@ package com.etb.filemanager.files.provider.archive.common.properties import android.annotation.SuppressLint +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -21,7 +22,6 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import com.etb.filemanager.R -import com.etb.filemanager.files.extensions.parcelable import com.etb.filemanager.files.provider.archive.common.mime.MediaType @@ -35,7 +35,7 @@ private const val ARG_FILE_PROPERTIES = "fileProperties" * create an instance of this fragment. * teste */ -class BasicPropertiesFragment() : Fragment() { +class BasicPropertiesFragment : Fragment() { // TODO: Rename and change types of parameters private var param1: String? = null private var param2: String? = null @@ -48,8 +48,12 @@ class BasicPropertiesFragment() : Fragment() { arguments?.let { param1 = it.getString(ARG_PARAM1) param2 = it.getString(ARG_PARAM2) - fileProperties = it.parcelable(ARG_FILE_PROPERTIES) - + fileProperties = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + it.getParcelableArrayList(ARG_FILE_PROPERTIES, FileProperties::class.java) + } else { + @Suppress("DEPRECATION") + it.getParcelableArrayList(ARG_FILE_PROPERTIES) + } } } @@ -77,7 +81,7 @@ class BasicPropertiesFragment() : Fragment() { * @author Juan Nascimento */ - fun addListProperties(fileBasicProperties: MutableList) { + private fun addListProperties(fileBasicProperties: MutableList) { for (properties in fileBasicProperties) { addProperties(properties.title, properties.property, properties.isMedia, properties.mediaType, properties.mediaPath) } diff --git a/app/src/main/java/com/etb/filemanager/fragment/RecentFragment.kt b/app/src/main/java/com/etb/filemanager/fragment/RecentFragment.kt index e9f95ea3..bf2404ae 100644 --- a/app/src/main/java/com/etb/filemanager/fragment/RecentFragment.kt +++ b/app/src/main/java/com/etb/filemanager/fragment/RecentFragment.kt @@ -34,7 +34,9 @@ import androidx.recyclerview.widget.RecyclerView import com.etb.filemanager.R import com.etb.filemanager.activity.MainActivity import com.etb.filemanager.activity.SettingsActivity -import com.etb.filemanager.compose.feature.presentation.categorylist.CategoryListScreen +import com.etb.filemanager.compose.core.navigation.CategoryListRoute +import com.etb.filemanager.compose.core.navigation.ChatRoute +import com.etb.filemanager.compose.feature.presentation.HomeScreen import com.etb.filemanager.files.extensions.applyBackgroundFromPreferences import com.etb.filemanager.files.util.fileProviderUri import com.etb.filemanager.interfaces.manager.ItemListener @@ -50,6 +52,7 @@ import com.etb.filemanager.manager.util.MaterialDialogUtils import com.etb.filemanager.settings.preference.AboutFragment import com.etb.filemanager.ui.view.ModalBottomSheetAddCategory import com.google.android.material.card.MaterialCardView +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.progressindicator.CircularProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator import kotlinx.coroutines.* @@ -66,6 +69,7 @@ class RecentFragment : Fragment(), ItemListener { private lateinit var cBaseItem: MaterialCardView private lateinit var btnAddCategory: Button private lateinit var adapter: CategoryFileModelAdapter + private lateinit var fabChat: ExtendedFloatingActionButton override fun onCreateView( @@ -84,6 +88,7 @@ class RecentFragment : Fragment(), ItemListener { cRecentImg = view.findViewById(R.id.cRecentImage) btnAddCategory = view.findViewById(R.id.btnAddCategory) cInternalStorage = view.findViewById(R.id.cInternalStorage) + fabChat = view.findViewById(R.id.fab_chat) val mnAbout = view.findViewById(R.id.mn_about) val aboutFragment = AboutFragment() @@ -157,6 +162,13 @@ class RecentFragment : Fragment(), ItemListener { startActivity(settingsIntent) } btnAddCategory.setOnClickListener { showBottomSheetAddCategory() } + + fabChat.setOnClickListener { + val intent = Intent(requireActivity(), HomeScreen::class.java).apply { + putExtra("startDestination", ChatRoute) + } + startActivity(intent) + } } private fun openListFiles() { @@ -342,8 +354,9 @@ class RecentFragment : Fragment(), ItemListener { val homeFragment = HomeFragment.newInstance(uri) (requireActivity() as MainActivity).startNewFragment(homeFragment) } else { - val intent = Intent(requireContext(), CategoryListScreen::class.java) + val intent = Intent(requireContext(), HomeScreen::class.java) intent.putExtra("categoryFileModel", categoryFileModel) + intent.putExtra("startDestination", CategoryListRoute) requireActivity().startActivity(intent) } diff --git a/app/src/main/java/com/etb/filemanager/manager/files/filecoroutine/FileOperation.kt b/app/src/main/java/com/etb/filemanager/manager/files/filecoroutine/FileOperation.kt index b9618391..b17d364c 100644 --- a/app/src/main/java/com/etb/filemanager/manager/files/filecoroutine/FileOperation.kt +++ b/app/src/main/java/com/etb/filemanager/manager/files/filecoroutine/FileOperation.kt @@ -45,13 +45,17 @@ import kotlin.io.path.moveTo suspend fun performFileOperation( operation: FileOperation, - sourcePath: List?, - newNames: List?, - createDir: Boolean?, - destinationPath: String?, - compressionType: CompressionType?, - progressListener: (Int) -> Unit, - completionListener: (Boolean) -> Unit + sourcePath: List? = null, + newNames: List? = null, + createDir: Boolean? = null, + destinationPath: String? = null, + compressionType: CompressionType? = null, + progressListener: (Int) -> Unit = { + + }, + completionListener: (Boolean) -> Unit = { + + } ) { withContext(Dispatchers.IO) { try { @@ -92,7 +96,7 @@ private fun deleteFile( val progress = (completedFiles * 100 / totalFiles).toInt() sendProgress(progressListener, progress) } catch (e: IOException) { - e.printStackTrace() + e.printStackTrace() } } } @@ -147,7 +151,7 @@ private fun moveAtomically(source: Path, target: Path) { source.moveTo(target, LinkOption.NOFOLLOW_LINKS, StandardCopyOption.ATOMIC_MOVE) } catch (e: InterruptedIOException) { - e.printStackTrace() + e.printStackTrace() } } @@ -253,9 +257,7 @@ class CompressFiles { } private fun compressToZip( - filesToArchive: List, - outputFilePath: String, - progressListener: (Int) -> Unit + filesToArchive: List, outputFilePath: String, progressListener: (Int) -> Unit ) { val outputZipFile = File(outputFilePath) val outputStream = FileOutputStream(outputZipFile) @@ -317,9 +319,7 @@ class CompressFiles { } private fun compressToSevenZ( - filesToArchive: List, - outputFilePath: String, - progressListener: (Int) -> Unit + filesToArchive: List, outputFilePath: String, progressListener: (Int) -> Unit ) { val outputSevenZFile = File(outputFilePath) val outputStream = FileOutputStream(outputSevenZFile) @@ -372,11 +372,7 @@ class CompressFiles { if (children != null && children.isNotEmpty()) { for (childFile in children) { addFileToSevenZ( - sevenZOutputFile, - childFile, - entryName, - totalSize, - progressListener + sevenZOutputFile, childFile, entryName, totalSize, progressListener ) } } else { @@ -388,9 +384,7 @@ class CompressFiles { } private fun compressToTar( - filesToArchive: List, - outputFilePath: String, - progressListener: (Int) -> Unit + filesToArchive: List, outputFilePath: String, progressListener: (Int) -> Unit ) { val outputTarFile = File(outputFilePath) val outputStream = FileOutputStream(outputTarFile) @@ -453,9 +447,7 @@ class CompressFiles { private fun compressToXz( - filesToArchive: List, - outputFilePath: String, - progressListener: (Int) -> Unit + filesToArchive: List, outputFilePath: String, progressListener: (Int) -> Unit ) { val outputXzFile = File(outputFilePath) val outputStream = FileOutputStream(outputXzFile) @@ -480,9 +472,7 @@ class CompressFiles { } private fun compressToGzip( - filesToArchive: List, - outputFilePath: String, - progressListener: (Int) -> Unit + filesToArchive: List, outputFilePath: String, progressListener: (Int) -> Unit ) { val outputGzipFile = File(outputFilePath) val outputStream = FileOutputStream(outputGzipFile) @@ -546,8 +536,7 @@ class ExtractArchives { } } - private fun extractZipArchive(archiveFile: File, outputDirFile: File) { - /* val zipInputStream = + private fun extractZipArchive(archiveFile: File, outputDirFile: File) {/* val zipInputStream = ArchiveStreamFactory().createArchiveInputStream("zip", FileInputStream(archiveFile)) var entry: ArchiveEntry? = zipInputStream.nextEntry diff --git a/app/src/main/java/com/etb/filemanager/settings/preference/BehaviorPreferences.kt b/app/src/main/java/com/etb/filemanager/settings/preference/BehaviorPreferences.kt index bf282c97..d139ea16 100644 --- a/app/src/main/java/com/etb/filemanager/settings/preference/BehaviorPreferences.kt +++ b/app/src/main/java/com/etb/filemanager/settings/preference/BehaviorPreferences.kt @@ -97,5 +97,4 @@ class BehaviorPreferences : PreferenceFragment() { ).picker() } -} - +} \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_recent.xml b/app/src/main/res/layout-land/fragment_recent.xml index 999e55c7..983180fd 100644 --- a/app/src/main/res/layout-land/fragment_recent.xml +++ b/app/src/main/res/layout-land/fragment_recent.xml @@ -23,7 +23,7 @@ + + + @@ -226,210 +227,217 @@ - - - - - - - - - - - - - - - - - - + - - - - - -