forked from Writand/writand
Merge branch 'frontend/project-picker' into 'main'
feat: project picker startup Closes #11 See merge request EmmaVandewalle/writand!45
This commit is contained in:
commit
dd31c82f75
22 changed files with 561 additions and 39 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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>
|
|
||||||
)
|
)
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
|
@ -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)
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 ?: "./"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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\"?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue