From 6348f571efcbbc1bd866c54aef43118a57bfc0de Mon Sep 17 00:00:00 2001 From: Emma Vandewalle Date: Mon, 11 Nov 2024 20:44:07 +0000 Subject: [PATCH] feat: project picker startup --- app/build.gradle | 2 +- .../writand/data/local/db/OpenedFilesDao.kt | 33 +++ .../data/local/db/OpenedFilesDatabase.kt | 25 ++ .../data/local/db/OpenedFilesEntity.kt | 31 ++ .../re/writand/data/local/db/ProjectEntity.kt | 9 +- .../re/writand/data/local/db/ProjectsDao.kt | 6 +- .../re/writand/data/local/models/Project.kt | 8 +- .../repos/projects/IOpenedFilesRepository.kt | 29 ++ .../repos/projects/IProjectsRepository.kt | 4 +- .../repos/projects/OpenedFilesRepository.kt | 27 ++ .../data/repos/projects/ProjectsRepository.kt | 8 +- .../java/be/re/writand/di/DatabaseModule.kt | 14 + .../projects/AddRecentProjectUseCase.kt | 19 ++ .../projects/GetRecentProjectsUseCase.kt | 20 ++ .../be/re/writand/navigation/WNavGraph.kt | 15 +- .../re/writand/screens/components/PopUps.kt | 2 +- .../re/writand/screens/components/WPopup.kt | 5 +- .../re/writand/screens/editor/EditorScreen.kt | 4 +- .../screens/filetree/WTreeComponent.kt | 9 - .../projectpicker/ProjectPickerScreen.kt | 273 +++++++++++++++++- .../projectpicker/ProjectPickerUiState.kt | 13 + .../projectpicker/ProjectPickerViewModel.kt | 44 +++ 22 files changed, 561 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/be/re/writand/data/local/db/OpenedFilesDao.kt create mode 100644 app/src/main/java/be/re/writand/data/local/db/OpenedFilesDatabase.kt create mode 100644 app/src/main/java/be/re/writand/data/local/db/OpenedFilesEntity.kt create mode 100644 app/src/main/java/be/re/writand/data/repos/projects/IOpenedFilesRepository.kt create mode 100644 app/src/main/java/be/re/writand/data/repos/projects/OpenedFilesRepository.kt create mode 100644 app/src/main/java/be/re/writand/domain/projects/AddRecentProjectUseCase.kt create mode 100644 app/src/main/java/be/re/writand/domain/projects/GetRecentProjectsUseCase.kt create mode 100644 app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerUiState.kt create mode 100644 app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index b93b4a0..30da443 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,7 +99,7 @@ dependencies { // Room implementation 'androidx.room:room-runtime:2.6.1' - annotationProcessor 'androidx.room:room-compiler:2.6.1' + kapt 'androidx.room:room-compiler:2.6.1' // Use kapt instead of annotationProcessor implementation 'androidx.room:room-ktx:2.6.1' // Proto DataStore diff --git a/app/src/main/java/be/re/writand/data/local/db/OpenedFilesDao.kt b/app/src/main/java/be/re/writand/data/local/db/OpenedFilesDao.kt new file mode 100644 index 0000000..01bb562 --- /dev/null +++ b/app/src/main/java/be/re/writand/data/local/db/OpenedFilesDao.kt @@ -0,0 +1,33 @@ +package be.re.writand.data.local.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import be.re.writand.data.local.models.OpenFilePath +import kotlinx.coroutines.flow.Flow + +@Dao +interface OpenedFilesDao { + + /** + * Get all the opened files from a project. + */ + @Query("SELECT * FROM openedFiles WHERE projectId = :fromProjectId") + fun getAllOpenedFiles(fromProjectId: Int): Flow> + + /** + * Insert a file into the database that was just opened. + * @param[openFilePath] the OpenFilePath to add in the database. + */ + @Insert(entity = OpenedFilesEntity::class) + suspend fun insertOpenedFilePath(openFilePath: OpenFilePath) + + /** + * Delete a OpenFilePath from the open files of a project. + * @param[openFilePathId] the OpenFilePath to be deleted from the database. + */ + @Query("DELETE FROM openedFiles where id = :openFilePathId") + suspend fun deleteOpenFilePath(openFilePathId: Int) + +} diff --git a/app/src/main/java/be/re/writand/data/local/db/OpenedFilesDatabase.kt b/app/src/main/java/be/re/writand/data/local/db/OpenedFilesDatabase.kt new file mode 100644 index 0000000..f6fbb88 --- /dev/null +++ b/app/src/main/java/be/re/writand/data/local/db/OpenedFilesDatabase.kt @@ -0,0 +1,25 @@ +package be.re.writand.data.local.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +/** + * Database for all the open files corresponding to the saved projects. + */ +@Database(entities = [OpenedFilesEntity::class, ProjectEntity::class], version = 1) +abstract class OpenedFilesDatabase: RoomDatabase() { + + abstract fun openedFilesDao(): OpenedFilesDao + + companion object { + @Volatile private var instance: OpenedFilesDatabase? = null + + fun getInstance(context: Context) = instance ?: synchronized(this) { + return Room.databaseBuilder( + context.applicationContext, OpenedFilesDatabase::class.java, "openedFiles-db" + ).build().also { instance = it } + } + } +} diff --git a/app/src/main/java/be/re/writand/data/local/db/OpenedFilesEntity.kt b/app/src/main/java/be/re/writand/data/local/db/OpenedFilesEntity.kt new file mode 100644 index 0000000..21b556e --- /dev/null +++ b/app/src/main/java/be/re/writand/data/local/db/OpenedFilesEntity.kt @@ -0,0 +1,31 @@ +package be.re.writand.data.local.db + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +/** + * Entity for storing the opened files of a project. + * @param[projectId] the id of the project the file belongs to. + * @param[path] the absolute path to the file starting from the root of the project. + * @param[isSaved] info whether the file was saved before closing the app (for expansion of unsaved files). + */ +@Entity( + tableName = "openedFiles", + foreignKeys = [ + ForeignKey( + entity = ProjectEntity::class, + parentColumns = arrayOf("projectId"), + childColumns = arrayOf("projectId"), + onDelete = ForeignKey.CASCADE + ) + ], + indices = [Index(value = ["projectId"])] +) +data class OpenedFilesEntity ( + @PrimaryKey(autoGenerate = true) val id: Int, + val projectId: Int, + val path: String, + val isSaved: Boolean +) diff --git a/app/src/main/java/be/re/writand/data/local/db/ProjectEntity.kt b/app/src/main/java/be/re/writand/data/local/db/ProjectEntity.kt index 6ccf42e..2661b97 100644 --- a/app/src/main/java/be/re/writand/data/local/db/ProjectEntity.kt +++ b/app/src/main/java/be/re/writand/data/local/db/ProjectEntity.kt @@ -1,19 +1,18 @@ package be.re.writand.data.local.db import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.PrimaryKey import be.re.writand.data.local.models.OpenFilePath /** * Entity for recent projects to keep in memory, used in the ProjectsDAO. - * @param[id] the unique id of a project. + * @param[projectId] the unique id of a project. * @param[path] the absolute path to the directory/file. * A file in case of a project consisting of 1 file only. - * @param[openedFiles] a list of objects to specify more about each last opened file. */ @Entity(tableName = "projects") data class ProjectEntity ( - @PrimaryKey val id: Int, - val path: String, - val openedFiles: List + @PrimaryKey(autoGenerate = true) val projectId: Int, + val path: String ) \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/data/local/db/ProjectsDao.kt b/app/src/main/java/be/re/writand/data/local/db/ProjectsDao.kt index ce54be0..8d1c0ec 100644 --- a/app/src/main/java/be/re/writand/data/local/db/ProjectsDao.kt +++ b/app/src/main/java/be/re/writand/data/local/db/ProjectsDao.kt @@ -29,9 +29,9 @@ interface ProjectsDao { /** * Delete a project from the recent opened projects. * Should be called when the limit of saving projects is reached. - * @param[project] the project to be deleted from the database. + * @param[id] the project id to be deleted from the database. */ - @Delete(entity = ProjectEntity::class) - suspend fun deleteProject(project: Project) + @Query("DELETE FROM projects where projectId = :id") + suspend fun deleteProject(id: Int) } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/data/local/models/Project.kt b/app/src/main/java/be/re/writand/data/local/models/Project.kt index 063eb24..39dd38c 100644 --- a/app/src/main/java/be/re/writand/data/local/models/Project.kt +++ b/app/src/main/java/be/re/writand/data/local/models/Project.kt @@ -2,15 +2,15 @@ package be.re.writand.data.local.models /** * Data class used in the ProjectsDAO to store information about a recent project. - * @param[id] a unique id for the project. + * @param[projectId] a unique id for the project. * @param[path] the absolute path to the directory/file location of the project. */ -data class Project(val id: Int, val path: String) +data class Project(val projectId: Int, val path: String) /** * Data class used in ProjectEntity to store information about a recent file of a project. * @param[projectId] the id of the project the file belongs to. - * @param[path] the relative path to the file starting from the root fo the project. + * @param[path] the relative path to the file starting from the root of the project. * @param[isSaved] info whether the file was saved before closing the app (for expansion of unsaved files). */ -data class OpenFilePath(val projectId: Int, val path: String, var isSaved: Boolean) \ No newline at end of file +data class OpenFilePath(val id: Int, val projectId: Int, val path: String, var isSaved: Boolean) \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/data/repos/projects/IOpenedFilesRepository.kt b/app/src/main/java/be/re/writand/data/repos/projects/IOpenedFilesRepository.kt new file mode 100644 index 0000000..9439a8e --- /dev/null +++ b/app/src/main/java/be/re/writand/data/repos/projects/IOpenedFilesRepository.kt @@ -0,0 +1,29 @@ +package be.re.writand.data.repos.projects + +import be.re.writand.data.local.models.OpenFilePath +import kotlinx.coroutines.flow.Flow + + +/** + * Repository that handles everything for the local OpenedFilesDatabase. + * @param[dao] the dao provides get/add/delete functionality of the opened files from a project. + */ +interface IOpenedFilesRepository { + + /** + * Gets all the opened files from the local storage. + */ + fun getAllOpenedFiles(fromProjectId: Int): Flow> + + /** + * Adds a file to the local storage of a recent project. + * @param[openFilePath] the OpenedFilePath to be added to the database. + */ + suspend fun addOpenedFilePath(openFilePath: OpenFilePath) + + /** + * Deletes a file from the local storage of opened files of a project. + * @param[openFilePathId] the OpenedFilePath to be deleted from the database. + */ + suspend fun deleteOpenFilePath(openFilePathId: Int) +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/data/repos/projects/IProjectsRepository.kt b/app/src/main/java/be/re/writand/data/repos/projects/IProjectsRepository.kt index c7f84b6..349ccdb 100644 --- a/app/src/main/java/be/re/writand/data/repos/projects/IProjectsRepository.kt +++ b/app/src/main/java/be/re/writand/data/repos/projects/IProjectsRepository.kt @@ -23,7 +23,7 @@ interface IProjectsRepository { /** * Deletes a project from the local storage of recent projects. - * @param[project] the project to be deleted from the database. + * @param[projectId] the project to be deleted from the database. */ - suspend fun deleteProject(project: Project) + suspend fun deleteProject(projectId: Int) } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/data/repos/projects/OpenedFilesRepository.kt b/app/src/main/java/be/re/writand/data/repos/projects/OpenedFilesRepository.kt new file mode 100644 index 0000000..12a2ebf --- /dev/null +++ b/app/src/main/java/be/re/writand/data/repos/projects/OpenedFilesRepository.kt @@ -0,0 +1,27 @@ +package be.re.writand.data.repos.projects + +import be.re.writand.data.local.db.OpenedFilesDao +import be.re.writand.data.local.models.OpenFilePath +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Repository that handles everything for the local OpenedFilesDatabase. + * @param[dao] the dao provides get/add/delete functionality of the opened files from a project. + */ +@Singleton +class OpenedFilesRepository @Inject constructor(private val dao: OpenedFilesDao): IOpenedFilesRepository { + + override fun getAllOpenedFiles(fromProjectId: Int): Flow> { + return dao.getAllOpenedFiles(fromProjectId) + } + + override suspend fun addOpenedFilePath(openFilePath: OpenFilePath) { + dao.insertOpenedFilePath(openFilePath) + } + + override suspend fun deleteOpenFilePath(openFilePathId: Int) { + dao.deleteOpenFilePath(openFilePathId) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/data/repos/projects/ProjectsRepository.kt b/app/src/main/java/be/re/writand/data/repos/projects/ProjectsRepository.kt index a6b76d4..75a2f21 100644 --- a/app/src/main/java/be/re/writand/data/repos/projects/ProjectsRepository.kt +++ b/app/src/main/java/be/re/writand/data/repos/projects/ProjectsRepository.kt @@ -6,6 +6,10 @@ import kotlinx.coroutines.flow.Flow import javax.inject.Inject import javax.inject.Singleton +/** + * Repository that handles everything for the local ProjectsDatabase. + * @param[dao] the dao provides get/add/delete functionality of the recent projects. + */ @Singleton class ProjectsRepository @Inject constructor(private val dao: ProjectsDao): IProjectsRepository { @@ -17,7 +21,7 @@ class ProjectsRepository @Inject constructor(private val dao: ProjectsDao): IPro dao.insertProject(project) } - override suspend fun deleteProject(project: Project) { - dao.deleteProject(project) + override suspend fun deleteProject(projectId: Int) { + dao.deleteProject(projectId) } } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/di/DatabaseModule.kt b/app/src/main/java/be/re/writand/di/DatabaseModule.kt index c65ad2c..2b93d37 100644 --- a/app/src/main/java/be/re/writand/di/DatabaseModule.kt +++ b/app/src/main/java/be/re/writand/di/DatabaseModule.kt @@ -1,6 +1,8 @@ package be.re.writand.di import android.content.Context +import be.re.writand.data.local.db.OpenedFilesDao +import be.re.writand.data.local.db.OpenedFilesDatabase import be.re.writand.data.local.filemanager.FileManagerLocal import be.re.writand.data.local.filemanager.IFileManager import be.re.writand.data.local.db.ProjectsDao @@ -28,11 +30,23 @@ class DatabaseModule { fun provideProjectsDatabase(@ApplicationContext context: Context): ProjectsDatabase { return ProjectsDatabase.getInstance(context) } + @Provides fun provideProjectsDao(projectsDatabase: ProjectsDatabase): ProjectsDao { return projectsDatabase.projectsDao() } + @Singleton + @Provides + fun provideOpenedFilesDatabase(@ApplicationContext context: Context): OpenedFilesDatabase { + return OpenedFilesDatabase.getInstance(context) + } + + @Provides + fun provideOpenedFilesDao(openedFilesDatabase: OpenedFilesDatabase): OpenedFilesDao { + return openedFilesDatabase.openedFilesDao() + } + /** * Provide the application context for TOSRepo. */ diff --git a/app/src/main/java/be/re/writand/domain/projects/AddRecentProjectUseCase.kt b/app/src/main/java/be/re/writand/domain/projects/AddRecentProjectUseCase.kt new file mode 100644 index 0000000..0e2311d --- /dev/null +++ b/app/src/main/java/be/re/writand/domain/projects/AddRecentProjectUseCase.kt @@ -0,0 +1,19 @@ +package be.re.writand.domain.projects + +import be.re.writand.data.local.models.Project +import be.re.writand.data.repos.projects.ProjectsRepository +import javax.inject.Inject + +/** + * Use-case that adds a project to the local storage of recent projects. + * @param[projectsRepository] Repository to interact with the projects data source. + */ +class AddRecentProjectUseCase @Inject constructor( + private val projectsRepository: ProjectsRepository +) { + + suspend operator fun invoke(project: Project) { + projectsRepository.addProject(project) + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/domain/projects/GetRecentProjectsUseCase.kt b/app/src/main/java/be/re/writand/domain/projects/GetRecentProjectsUseCase.kt new file mode 100644 index 0000000..2b3b27c --- /dev/null +++ b/app/src/main/java/be/re/writand/domain/projects/GetRecentProjectsUseCase.kt @@ -0,0 +1,20 @@ +package be.re.writand.domain.projects + +import be.re.writand.data.local.models.Project +import be.re.writand.data.repos.projects.ProjectsRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +/** + * Use-case that gets all the recent projects from the local storage. + * @param[projectsRepository] the repository that provides the projects. + */ +class GetRecentProjectsUseCase @Inject constructor( + private val projectsRepository: ProjectsRepository +) { + + operator fun invoke(): Flow> { + return projectsRepository.getAllProjects() + } + +} \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/navigation/WNavGraph.kt b/app/src/main/java/be/re/writand/navigation/WNavGraph.kt index 42246b3..5c8d0c5 100644 --- a/app/src/main/java/be/re/writand/navigation/WNavGraph.kt +++ b/app/src/main/java/be/re/writand/navigation/WNavGraph.kt @@ -1,5 +1,6 @@ package be.re.writand.navigation +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost @@ -11,6 +12,7 @@ import be.re.writand.screens.splash.SplashScreen import be.re.writand.screens.welcome.WelcomeSettingsScreen import be.re.writand.screens.welcome.WelcomeStartScreen import be.re.writand.screens.welcome.WelcomeTOSScreen +import java.net.URLDecoder /** * The navigation controller containing all the different screens a user can go to. @@ -60,8 +62,17 @@ fun WNavGraph(navHostController: NavHostController, modifier: Modifier) { // Editor view - composable(WAppDestinations.MAIN_EDITOR) { - EditorScreen(navHostController = navHostController) + composable(WAppDestinations.MAIN_EDITOR + "/{projectId}/{path}") { entry -> + val projectId: Int? = entry.arguments?.getInt("projectId") + val path: String? = + entry.arguments?.getString("path")?.let { URLDecoder.decode(it, "UTF-8") } + if (projectId != null) { + EditorScreen( + navHostController = navHostController, + projectId = projectId, + path = path ?: "./" + ) + } } } diff --git a/app/src/main/java/be/re/writand/screens/components/PopUps.kt b/app/src/main/java/be/re/writand/screens/components/PopUps.kt index bc2de0c..af41ab3 100644 --- a/app/src/main/java/be/re/writand/screens/components/PopUps.kt +++ b/app/src/main/java/be/re/writand/screens/components/PopUps.kt @@ -47,7 +47,7 @@ enum class CreationType { * @param[onDismiss] a function belonging to the exit button if it needs to be shown (default is the empty function). */ @Composable -private fun TopPopUpBar(title: String, showExit: Boolean = true, onDismiss: () -> Unit = {}) { +fun TopPopUpBar(title: String, showExit: Boolean = true, onDismiss: () -> Unit = {}) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/be/re/writand/screens/components/WPopup.kt b/app/src/main/java/be/re/writand/screens/components/WPopup.kt index 18292ce..bc710b5 100644 --- a/app/src/main/java/be/re/writand/screens/components/WPopup.kt +++ b/app/src/main/java/be/re/writand/screens/components/WPopup.kt @@ -3,7 +3,6 @@ package be.re.writand.screens.components import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width @@ -11,11 +10,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.FabPosition import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.ui.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -75,4 +72,4 @@ fun WPopup( } } -} \ No newline at end of file +} diff --git a/app/src/main/java/be/re/writand/screens/editor/EditorScreen.kt b/app/src/main/java/be/re/writand/screens/editor/EditorScreen.kt index 1ba38a5..046a142 100644 --- a/app/src/main/java/be/re/writand/screens/editor/EditorScreen.kt +++ b/app/src/main/java/be/re/writand/screens/editor/EditorScreen.kt @@ -27,6 +27,8 @@ import be.re.writand.screens.settings.SettingsPopup @Composable fun EditorScreen( navHostController: NavHostController, + projectId: Int, + path: String, vm: EditorScreenViewModel = hiltViewModel() ) { // state for filetree view @@ -48,7 +50,7 @@ fun EditorScreen( Row(modifier = Modifier.padding(it).padding(10.dp)) { if(isOpened) { WFiletree( - root = "/storage/emulated/0/Documents/webserver", + root = path, modifier = Modifier.weight(1F), onSelect = { path -> vm.setOpenedFile(path) diff --git a/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt b/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt index 790cb2f..666f320 100644 --- a/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt +++ b/app/src/main/java/be/re/writand/screens/filetree/WTreeComponent.kt @@ -1,31 +1,23 @@ package be.re.writand.screens.filetree -import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,7 +37,6 @@ import be.re.writand.screens.components.WDangerButton import be.re.writand.screens.components.WLabelAndTextField import be.re.writand.screens.components.WPopup import be.re.writand.screens.components.WText -import be.re.writand.ui.theme.MainGreen import be.re.writand.utils.FileNode import java.nio.file.Path diff --git a/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerScreen.kt b/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerScreen.kt index 919fde4..cda8b33 100644 --- a/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerScreen.kt +++ b/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerScreen.kt @@ -1,23 +1,286 @@ package be.re.writand.screens.projectpicker +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectTapGestures +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.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.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold 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.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController +import be.re.writand.data.local.models.Project import be.re.writand.navigation.WAppDestinations +import be.re.writand.screens.WUIGlobals +import be.re.writand.screens.components.TopPopUpBar +import be.re.writand.screens.components.WBorderButton import be.re.writand.screens.components.WButton +import be.re.writand.screens.components.WLoadingIndicator +import be.re.writand.screens.components.WLogoImage +import be.re.writand.screens.components.WPopup +import be.re.writand.screens.components.WText +import be.re.writand.screens.directoryprovider.DirectoryProvider +import kotlinx.coroutines.delay +import java.io.File +import java.net.URLEncoder +/** + * A composable that shows a list of projects and allows the user to open them. + * The screen has two parts: a list of recent projects, and a set of buttons + * to open a project or create a new one. + * @param[navHostController] the navigation host controller used to navigate to other screens. + * @param[vM] the view model of the screen, used to get the projects. + */ @Composable fun ProjectPickerScreen( - navHostController: NavHostController + navHostController: NavHostController, + vM: ProjectPickerViewModel = hiltViewModel() ) { + val projects by vM.projects.collectAsState() + val openDialog = remember { mutableStateOf(false) } - Box(modifier = Modifier.size(20.dp, 20.dp)) { - WButton( - text = "Create new", - onClick = { navHostController.navigate(WAppDestinations.MAIN_EDITOR) } + Box( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.secondary), + contentAlignment = Alignment.Center + ) { + Scaffold(modifier = Modifier + .size(600.dp) + .border( + 1.dp, + color = MaterialTheme.colorScheme.tertiary, + shape = RoundedCornerShape(WUIGlobals.cornerRadius) + ) + .clip(shape = RoundedCornerShape(WUIGlobals.cornerRadius)), + topBar = { + Box(modifier = Modifier.height(75.dp)) { + WLogoImage( + modifier = Modifier.padding(25.dp), + alignment = Alignment.CenterStart + ) + } + } + ) { + Column( + modifier = Modifier + .padding(it) + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + WText( + text = "Recent projects", + settingsFontSizeAlterBy = WUIGlobals.HEADING + ) + + Column( + modifier = Modifier + .size(width = 500.dp, height = 400.dp) + ) { + Box( + modifier = Modifier + .height(350.dp) + .fillMaxWidth() + .border( + 1.dp, + color = MaterialTheme.colorScheme.tertiary, + shape = RoundedCornerShape(WUIGlobals.cornerRadius) + ) + .clip(shape = RoundedCornerShape(WUIGlobals.cornerRadius)), + ) { + when (val value = projects) { + ProjectPickerUiState.Loading -> { + WLoadingIndicator() + } + + is ProjectPickerUiState.Success -> { + LazyColumn { + items(value.projects.size) { project -> + ProjectItem(value.projects[project], navHostController) + } + } + } + } + } + Row( + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.End + ) { + WButton("Open/Create Project", onClick = { + openDialog.value = !openDialog.value + }) + } + } + } + } + } + + if (openDialog.value) { + DirectoryProvider( + onDismiss = { openDialog.value = false }, + onConfirm = { + val newProject = Project(projectId = vM.amountOfProjects, path = it) + vM.addProject(newProject) + navHostController.navigate( + WAppDestinations.MAIN_EDITOR + + "/${newProject.projectId}/${ + URLEncoder.encode( + newProject.path, + "UTF-8" + ) + }" + ) + } ) } +} + +/** + * A composable that shows a single project. + * @param[project] the project to show. + * @param[navHostController] the navigation host controller to navigate to the editor screen. + */ +@Composable +fun ProjectItem( + project: Project, + navHostController: NavHostController +) { + val showProjectConfirmation = remember { mutableStateOf(false) } + val name = project.path.split(File.separator).last() + + var isClicked by remember { mutableStateOf(false) } + + Column { + Row( + modifier = Modifier + .background( + color = if (isClicked) MaterialTheme.colorScheme.secondary + else MaterialTheme.colorScheme.primary + ) + .fillMaxWidth() + .padding(vertical = 15.dp, horizontal = 10.dp) + .pointerInput(Unit) { + detectTapGestures( + onTap = { + isClicked = true + }, + onDoubleTap = { + isClicked = true + showProjectConfirmation.value = true + } + ) + }, + horizontalArrangement = Arrangement.SpaceBetween + ) { + WText(text = name) + WText(text = project.path) + } + HorizontalDivider() + } + + if (showProjectConfirmation.value) { + PickProjectPopup( + name = name, + project = project, + onDismiss = { showProjectConfirmation.value = false }, + navHostController = navHostController + ) + } + + LaunchedEffect(isClicked) { + if (isClicked) { + delay(200L) + isClicked = false + } + } +} + +/** + * A pop up asking for confirmation before opening a project. + * @param[name] the name of the project to be opened. + * @param[project] the project to be opened. + * @param[onDismiss] a function to be called when the pop up is to be closed. + * @param[navHostController] the navigation host controller used to navigate to the editor screen. + */ +@Composable +fun PickProjectPopup( + name: String, + project: Project, + onDismiss: () -> Unit, + navHostController: NavHostController +) { + WPopup( + titleBar = { + TopPopUpBar(title = "Open \"$name\"", onDismiss = onDismiss) + }, + width = 500.dp, + height = 250.dp, + onDismiss = onDismiss, + bottomBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + WBorderButton( + text = "Cancel", + onClick = onDismiss + ) + + WButton( + text = "Continue", + onClick = { + onDismiss() + navHostController.navigate( + WAppDestinations.MAIN_EDITOR + + "/${project.projectId}/${ + URLEncoder.encode( + project.path, + "UTF-8" + ) + }" + ) + } + ) + } + } + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(it), + contentAlignment = Alignment.Center + ) { + WText(text = "Are you sure you want to open \"$name\"?") + } + } } \ No newline at end of file diff --git a/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerUiState.kt b/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerUiState.kt new file mode 100644 index 0000000..8875d97 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerUiState.kt @@ -0,0 +1,13 @@ +package be.re.writand.screens.projectpicker + +import be.re.writand.data.local.models.Project + +/** + * The ui state for the project picker. + * - [Loading] the content is still loading in. + * - [Success] the list of projects is ready. + */ +sealed interface ProjectPickerUiState { + object Loading: ProjectPickerUiState + data class Success(val projects: List): ProjectPickerUiState +} diff --git a/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerViewModel.kt b/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerViewModel.kt new file mode 100644 index 0000000..42b3a19 --- /dev/null +++ b/app/src/main/java/be/re/writand/screens/projectpicker/ProjectPickerViewModel.kt @@ -0,0 +1,44 @@ +package be.re.writand.screens.projectpicker + +import be.re.writand.data.local.models.Project +import be.re.writand.domain.projects.AddRecentProjectUseCase +import be.re.writand.domain.projects.GetRecentProjectsUseCase +import be.re.writand.screens.WViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +/** + * View model to be used by ProjectPickerScreen that handles the UserSettings and buttons. + * @param[getRecentProjects] use case to get the recent projects. + * @param[addRecentProject] use case to add a project to the recent projects. + */ +@HiltViewModel +class ProjectPickerViewModel @Inject constructor( + getRecentProjects: GetRecentProjectsUseCase, + private val addRecentProject: AddRecentProjectUseCase +): WViewModel() { + + private val _projects: MutableStateFlow = + MutableStateFlow(ProjectPickerUiState.Loading) + val projects: StateFlow = _projects + + private val _amountOfProjects: MutableStateFlow = MutableStateFlow(0); + val amountOfProjects: Int get() = _amountOfProjects.value + + init { + launchCatching { + getRecentProjects().collect { projects -> + _amountOfProjects.value = projects.size + _projects.value = ProjectPickerUiState.Success(projects) + } + } + } + + fun addProject(project: Project) { + launchCatching { + addRecentProject.invoke(project) + } + } +} \ No newline at end of file