Merge branch 'frontend/project-picker' into 'main'

feat: project picker startup

Closes #11

See merge request EmmaVandewalle/writand!45
This commit is contained in:
Emma Vandewalle 2024-11-11 20:44:07 +00:00
commit dd31c82f75
22 changed files with 561 additions and 39 deletions

View file

@ -99,7 +99,7 @@ dependencies {
// Room // Room
implementation 'androidx.room:room-runtime:2.6.1' 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' implementation 'androidx.room:room-ktx:2.6.1'
// Proto DataStore // Proto DataStore

View file

@ -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<List<OpenFilePath>>
/**
* 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)
}

View file

@ -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 }
}
}
}

View file

@ -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
)

View file

@ -1,19 +1,18 @@
package be.re.writand.data.local.db package be.re.writand.data.local.db
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import be.re.writand.data.local.models.OpenFilePath import be.re.writand.data.local.models.OpenFilePath
/** /**
* Entity for recent projects to keep in memory, used in the ProjectsDAO. * 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. * @param[path] the absolute path to the directory/file.
* A file in case of a project consisting of 1 file only. * 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") @Entity(tableName = "projects")
data class ProjectEntity ( data class ProjectEntity (
@PrimaryKey val id: Int, @PrimaryKey(autoGenerate = true) val projectId: Int,
val path: String, val path: String
val openedFiles: List<OpenFilePath>
) )

View file

@ -29,9 +29,9 @@ interface ProjectsDao {
/** /**
* Delete a project from the recent opened projects. * Delete a project from the recent opened projects.
* Should be called when the limit of saving projects is reached. * 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) @Query("DELETE FROM projects where projectId = :id")
suspend fun deleteProject(project: Project) suspend fun deleteProject(id: Int)
} }

View file

@ -2,15 +2,15 @@ package be.re.writand.data.local.models
/** /**
* Data class used in the ProjectsDAO to store information about a recent project. * 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. * @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. * 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[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). * @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) data class OpenFilePath(val id: Int, val projectId: Int, val path: String, var isSaved: Boolean)

View file

@ -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<List<OpenFilePath>>
/**
* 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)
}

View file

@ -23,7 +23,7 @@ interface IProjectsRepository {
/** /**
* Deletes a project from the local storage of recent projects. * 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)
} }

View file

@ -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<List<OpenFilePath>> {
return dao.getAllOpenedFiles(fromProjectId)
}
override suspend fun addOpenedFilePath(openFilePath: OpenFilePath) {
dao.insertOpenedFilePath(openFilePath)
}
override suspend fun deleteOpenFilePath(openFilePathId: Int) {
dao.deleteOpenFilePath(openFilePathId)
}
}

View file

@ -6,6 +6,10 @@ import kotlinx.coroutines.flow.Flow
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton 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 @Singleton
class ProjectsRepository @Inject constructor(private val dao: ProjectsDao): IProjectsRepository { 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) dao.insertProject(project)
} }
override suspend fun deleteProject(project: Project) { override suspend fun deleteProject(projectId: Int) {
dao.deleteProject(project) dao.deleteProject(projectId)
} }
} }

View file

@ -1,6 +1,8 @@
package be.re.writand.di package be.re.writand.di
import android.content.Context 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.FileManagerLocal
import be.re.writand.data.local.filemanager.IFileManager import be.re.writand.data.local.filemanager.IFileManager
import be.re.writand.data.local.db.ProjectsDao import be.re.writand.data.local.db.ProjectsDao
@ -28,11 +30,23 @@ class DatabaseModule {
fun provideProjectsDatabase(@ApplicationContext context: Context): ProjectsDatabase { fun provideProjectsDatabase(@ApplicationContext context: Context): ProjectsDatabase {
return ProjectsDatabase.getInstance(context) return ProjectsDatabase.getInstance(context)
} }
@Provides @Provides
fun provideProjectsDao(projectsDatabase: ProjectsDatabase): ProjectsDao { fun provideProjectsDao(projectsDatabase: ProjectsDatabase): ProjectsDao {
return 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. * Provide the application context for TOSRepo.
*/ */

View file

@ -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)
}
}

View file

@ -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<List<Project>> {
return projectsRepository.getAllProjects()
}
}

View file

@ -1,5 +1,6 @@
package be.re.writand.navigation package be.re.writand.navigation
import android.util.Log
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost 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.WelcomeSettingsScreen
import be.re.writand.screens.welcome.WelcomeStartScreen import be.re.writand.screens.welcome.WelcomeStartScreen
import be.re.writand.screens.welcome.WelcomeTOSScreen import be.re.writand.screens.welcome.WelcomeTOSScreen
import java.net.URLDecoder
/** /**
* The navigation controller containing all the different screens a user can go to. * 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 // Editor view
composable(WAppDestinations.MAIN_EDITOR) { composable(WAppDestinations.MAIN_EDITOR + "/{projectId}/{path}") { entry ->
EditorScreen(navHostController = navHostController) 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 ?: "./"
)
}
} }
} }

View file

@ -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). * @param[onDismiss] a function belonging to the exit button if it needs to be shown (default is the empty function).
*/ */
@Composable @Composable
private fun TopPopUpBar(title: String, showExit: Boolean = true, onDismiss: () -> Unit = {}) { fun TopPopUpBar(title: String, showExit: Boolean = true, onDismiss: () -> Unit = {}) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,

View file

@ -3,7 +3,6 @@ package be.re.writand.screens.components
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -11,11 +10,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog

View file

@ -27,6 +27,8 @@ import be.re.writand.screens.settings.SettingsPopup
@Composable @Composable
fun EditorScreen( fun EditorScreen(
navHostController: NavHostController, navHostController: NavHostController,
projectId: Int,
path: String,
vm: EditorScreenViewModel = hiltViewModel() vm: EditorScreenViewModel = hiltViewModel()
) { ) {
// state for filetree view // state for filetree view
@ -48,7 +50,7 @@ fun EditorScreen(
Row(modifier = Modifier.padding(it).padding(10.dp)) { Row(modifier = Modifier.padding(it).padding(10.dp)) {
if(isOpened) { if(isOpened) {
WFiletree( WFiletree(
root = "/storage/emulated/0/Documents/webserver", root = path,
modifier = Modifier.weight(1F), modifier = Modifier.weight(1F),
onSelect = { path -> onSelect = { path ->
vm.setOpenedFile(path) vm.setOpenedFile(path)

View file

@ -1,31 +1,23 @@
package be.re.writand.screens.filetree 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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add 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.Delete
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.WLabelAndTextField
import be.re.writand.screens.components.WPopup import be.re.writand.screens.components.WPopup
import be.re.writand.screens.components.WText import be.re.writand.screens.components.WText
import be.re.writand.ui.theme.MainGreen
import be.re.writand.utils.FileNode import be.re.writand.utils.FileNode
import java.nio.file.Path import java.nio.file.Path

View file

@ -1,23 +1,286 @@
package be.re.writand.screens.projectpicker 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.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.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.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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import be.re.writand.data.local.models.Project
import be.re.writand.navigation.WAppDestinations 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.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 @Composable
fun ProjectPickerScreen( 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)) { Box(
WButton( modifier = Modifier
text = "Create new", .background(color = MaterialTheme.colorScheme.secondary),
onClick = { navHostController.navigate(WAppDestinations.MAIN_EDITOR) } 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\"?")
}
}
}

View file

@ -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<Project>): ProjectPickerUiState
}

View file

@ -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<ProjectPickerUiState> =
MutableStateFlow(ProjectPickerUiState.Loading)
val projects: StateFlow<ProjectPickerUiState> = _projects
private val _amountOfProjects: MutableStateFlow<Int> = 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)
}
}
}