Compare commits

...

2 Commits

Author SHA1 Message Date
Raphael
9db5b0b3d7 Merge branch 'master' of https://git.kerboul.me/timelapse/timelapse-android
All checks were successful
Build APK and Upload Artifacts / build-apk (push) Successful in 24s
2025-04-27 17:21:13 +02:00
Raphael
9cb05d9457 appli finalized 2025-04-27 17:18:27 +02:00
21 changed files with 507 additions and 42 deletions

2
.idea/gradle.xml generated
View File

@@ -4,7 +4,6 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" /> <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">
@@ -13,6 +12,7 @@
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
</set> </set>
</option> </option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">

View File

@@ -83,3 +83,7 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
} }
kotlin {
tasks.register("testClasses")
}

BIN
app/release/app-release.apk Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.dreamteam.timelapse",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 28
}

View File

@@ -93,13 +93,27 @@ class MainActivity : AppCompatActivity() {
val builder: AlertDialog.Builder = AlertDialog.Builder(this) val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setView(layout) builder.setView(layout)
builder.setPositiveButton("Save", builder.setPositiveButton("Sauvegarder",
DialogInterface.OnClickListener { dialog, which -> DialogInterface.OnClickListener { dialog, which ->
val onSuccess : () -> Unit = {dialog.dismiss() ; Snackbar.make(viewfortoast, "Nouveau projet créé", Snackbar.LENGTH_LONG).setAnchorView(Rtmp.id.fab).show() ; } val onSuccess : () -> Unit = {
dialog.dismiss() ;
Snackbar.make(viewfortoast, "Nouveau projet créé", Snackbar.LENGTH_LONG).setAnchorView(Rtmp.id.fab).show() ;
}
val onError : (s:String) -> Unit = { s-> Snackbar.make(viewfortoast, "Erreur lors de la création du projet. ${s}", Snackbar.LENGTH_LONG).setAnchorView(Rtmp.id.fab).show() } val onError : (s:String) -> Unit = { s-> Snackbar.make(viewfortoast, "Erreur lors de la création du projet. ${s}", Snackbar.LENGTH_LONG).setAnchorView(Rtmp.id.fab).show() }
projectRepository.createProject(project_name.text.toString(), project_desc.text.toString(), onSuccess, onError) val name = project_name.text.toString()
val desc = project_desc.text.toString()
if (name.isBlank() || desc.isBlank() || name == "" || desc == "") {
Snackbar.make(viewfortoast, "Les champs ne doivent pas être vides", Snackbar.LENGTH_LONG)
.show()
}else {
projectRepository.createProject(
project_name.text.toString(),
project_desc.text.toString(),
onSuccess,
onError
)
}
}) })
builder.setNegativeButton("Cancel", builder.setNegativeButton("Cancel",
DialogInterface.OnClickListener { dialog, which -> dialog.dismiss() }) DialogInterface.OnClickListener { dialog, which -> dialog.dismiss() })

View File

@@ -1,14 +1,20 @@
package com.dreamteam.timelapse package com.dreamteam.timelapse
import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.dreamteam.timelapse.data.ApiService import com.dreamteam.timelapse.data.ApiService
import com.dreamteam.timelapse.data.CaptureQuery
import com.dreamteam.timelapse.data.Confirmation
import com.dreamteam.timelapse.data.Measurement import com.dreamteam.timelapse.data.Measurement
import com.dreamteam.timelapse.data.Project import com.dreamteam.timelapse.data.Project
import com.dreamteam.timelapse.data.ProjectRepository import com.dreamteam.timelapse.data.ProjectRepository
@@ -21,6 +27,11 @@ import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.formatter.ValueFormatter import com.github.mikephil.charting.formatter.ValueFormatter
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -30,6 +41,9 @@ import java.util.Locale
class ProjectActivity : AppCompatActivity() { class ProjectActivity : AppCompatActivity() {
private lateinit var stopingProcedureDialog: AlertDialog
private lateinit var videoRenderDialog: AlertDialog
private lateinit var startingProcedureDialog: AlertDialog
private lateinit var binding: ProjectBinding private lateinit var binding: ProjectBinding
private lateinit var projectRepository: ProjectRepository private lateinit var projectRepository: ProjectRepository
private lateinit var videoRepository: VideoRepository private lateinit var videoRepository: VideoRepository
@@ -37,6 +51,11 @@ class ProjectActivity : AppCompatActivity() {
private var measures: List<Measurement> = emptyList() // Déclare une liste de projets vide private var measures: List<Measurement> = emptyList() // Déclare une liste de projets vide
private var project: Project? = null private var project: Project? = null
private var imageUrls = emptyList<String>() private var imageUrls = emptyList<String>()
enum class ButtonState {
START, LOADING, FINISHED, ENDING
}
private var buttonState = ButtonState.START
class GridSpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() { class GridSpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
@@ -48,6 +67,16 @@ class ProjectActivity : AppCompatActivity() {
outRect.top = spacing outRect.top = spacing
} }
} }
fun refreshMultiuseButton(){
val floatingButton = findViewById<FloatingActionButton>(R.id.multiusefloating)
var icon = when (buttonState) {
ButtonState.START -> android.R.drawable.ic_media_play
ButtonState.LOADING -> R.drawable.baseline_stop_24
ButtonState.FINISHED, ButtonState.ENDING -> android.R.drawable.ic_menu_save
}
floatingButton.setImageResource(icon)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
.baseUrl("https://timelapse.kerboul.me/api/") .baseUrl("https://timelapse.kerboul.me/api/")
@@ -65,36 +94,77 @@ class ProjectActivity : AppCompatActivity() {
setContentView(binding.root) setContentView(binding.root)
initProjectInfo() initProjectInfo()
binding.imagesList.layoutManager = GridLayoutManager(this.baseContext, 2, GridLayoutManager.HORIZONTAL, false)
binding.imagesList.addItemDecoration(GridSpacingItemDecoration(16)) // 16px spacing
binding.videosList.layoutManager = GridLayoutManager(this.baseContext, 2, GridLayoutManager.HORIZONTAL, false)
binding.videosList.addItemDecoration(GridSpacingItemDecoration(16)) // 16px spacing
fetchMeasuresAndRebuildGraph() fetchMeasuresAndRebuildGraph()
fetchVideos()
val swipeRefreshLayout = binding.swipeRefreshLayout val swipeRefreshLayout = binding.swipeRefreshLayout
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener {
fetchMeasuresAndRebuildGraph() fetchMeasuresAndRebuildGraph()
fetchVideos()
Log.d("ProjetsFrag", "Actualisation des projets") Log.d("ProjetsFrag", "Actualisation des projets")
} }
fetchVideos()
val floatingButton = findViewById<FloatingActionButton>(R.id.multiusefloating)
this.buttonState = when(project?.getStatusText()) {
"Brouillon" -> ButtonState.START
"En Cours" -> ButtonState.LOADING
"Terminé" -> ButtonState.FINISHED
"En cours d'arret" -> ButtonState.ENDING
else -> ButtonState.START
}
var icon = when (buttonState) {
ButtonState.START -> android.R.drawable.ic_media_play
ButtonState.LOADING -> R.drawable.baseline_stop_24
ButtonState.FINISHED -> android.R.drawable.ic_menu_save
ButtonState.ENDING -> android.R.drawable.ic_menu_save
}
floatingButton.setImageResource(icon)
floatingButton.setOnClickListener {
// when (buttonState) {
// ButtonState.LOADING -> {
// buttonState = ButtonState.FINISHED
// }
// else -> {}
// }
// refreshMultiuseButton()
when (buttonState) {
ButtonState.START -> clickStartProcess()
ButtonState.LOADING -> clickStopProcess()
ButtonState.FINISHED, ButtonState.ENDING -> clickSave()
}
}
var but = findViewById<View>(R.id.multiusefloating)
but.post {
project?.let {
createVideoRenderDialog(but, it.id, measures)
createStopProcedureDialog(but, it.id)
createStartingProcedureDialog(but, it.id)
}
}
// Bouton flottant // Bouton flottant
// binding.fab.setOnClickListener { view -> // binding.fab.setOnClickListener { view ->
// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) //
// //.setAction("Action", null) //sert à rien
// .setAnchorView(R.id.fab).show() //au dessus du bouton mail
// } // }
} }
fun fetchVideos(){ fun fetchVideos(){
this.project?.let{ this.project?.let{
videoRepository.fetchVideosOfProject(it.id, onSuccess = { videos -> videoRepository.fetchRenderedVideosOfProject(it.id, onSuccess = { videos ->
val adapter = VideoAdapter(false, videos.toMutableList(), null) //ImageAdapter(imageUrls) val adapter = VideoAdapter(false, videos.toMutableList(), null) //ImageAdapter(imageUrls)
binding.videosList.layoutManager = GridLayoutManager(this.baseContext, 2, GridLayoutManager.HORIZONTAL, false)
binding.videosList.addItemDecoration(GridSpacingItemDecoration(16)) // 16px spacing
binding.videosList.adapter = adapter binding.videosList.adapter = adapter
binding.swipeRefreshLayout.isRefreshing = false
}, onError = {errorMessage -> }, onError = {errorMessage ->
Log.e("ProjectActivity", errorMessage) Log.e("ProjectActivity", "Fetch Video " + errorMessage)
fetchVideos() fetchVideos()
}) })
} }
@@ -109,8 +179,7 @@ class ProjectActivity : AppCompatActivity() {
initGraph(this.measures) initGraph(this.measures)
val adapter = ImageAdapter(this.measures.map { m-> "https://timelapse.kerboul.me/api/images/${m.project_id}/${m.order_id}"}) //ImageAdapter(imageUrls) val adapter = ImageAdapter(this.measures.map { m-> "https://timelapse.kerboul.me/api/images/${m.project_id}/${m.order_id}"}) //ImageAdapter(imageUrls)
binding.imagesList.layoutManager = GridLayoutManager(this.baseContext, 2, GridLayoutManager.HORIZONTAL, false) Log.d("ProjectActivity", "Je vais mettre l'adapter")
binding.imagesList.addItemDecoration(GridSpacingItemDecoration(16)) // 16px spacing
binding.imagesList.adapter = adapter binding.imagesList.adapter = adapter
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
@@ -124,7 +193,6 @@ class ProjectActivity : AppCompatActivity() {
}) })
} }
} }
fun initGraph(lm: List<Measurement>) { fun initGraph(lm: List<Measurement>) {
val temperatureHumidityChart = this.binding.temperatureHumidityChart val temperatureHumidityChart = this.binding.temperatureHumidityChart
@@ -160,8 +228,15 @@ class ProjectActivity : AppCompatActivity() {
val xAxis = temperatureHumidityChart.xAxis val xAxis = temperatureHumidityChart.xAxis
xAxis.valueFormatter = object : ValueFormatter() { xAxis.valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String { override fun getFormattedValue(value: Float): String {
// Map the index back to the corresponding timestamp val index = value.toInt()
val timestamp = lm.sortedBy { it.order_id }[value.toInt()].timestamp val sortedList = lm.sortedBy { it.order_id }
if (index < 0 || index >= sortedList.size) {
// Index out of bounds, return empty string or something neutral
return ""
}
val timestamp = sortedList[index].timestamp
val date = Date(timestamp.time) val date = Date(timestamp.time)
val sdf = SimpleDateFormat("dd/MM/yyyy:HH:mm:ss", Locale.getDefault()) val sdf = SimpleDateFormat("dd/MM/yyyy:HH:mm:ss", Locale.getDefault())
return sdf.format(date) return sdf.format(date)
@@ -222,9 +297,6 @@ class ProjectActivity : AppCompatActivity() {
// Refresh the chart // Refresh the chart
temperatureHumidityChart.invalidate() temperatureHumidityChart.invalidate()
} }
fun initProjectInfo(){ fun initProjectInfo(){
val nameview = findViewById<TextView>(R.id.project_name) val nameview = findViewById<TextView>(R.id.project_name)
val descriptionview = findViewById<TextView>(R.id.project_description) val descriptionview = findViewById<TextView>(R.id.project_description)
@@ -236,4 +308,200 @@ class ProjectActivity : AppCompatActivity() {
beginview.text = project!!.start_date.toString() beginview.text = project!!.start_date.toString()
statusview.text = project!!.getStatusText() statusview.text = project!!.getStatusText()
} }
private fun clickStartProcess() {
Log.i("ProjectActivity", "Start collect...")
// Simule une durée avant la finalisation du processus
// Handler(Looper.getMainLooper()).postDelayed({
// val floatingButton = findViewById<FloatingActionButton>(R.id.multiusefloating)
// floatingButton.setImageResource(android.R.drawable.ic_menu_save) // Icône d'enregistrement
// buttonState = ButtonState.FINISHED
// }, 5000) // 5 secondes de chargement
startingProcedureDialog.show()
}
private fun clickStopProcess() {
Log.i("ProjectActivity", "Stop collect...")
stopingProcedureDialog.show()
// Implémente l'arrêt du processus (annulation si nécessaire)
}
private fun clickSave() {
Log.i("ProjectActivity", "Create video...")
videoRenderDialog.show()
//videoRepository.createVideo(project.id, measures, )
// Toast.makeText(context, "Downloading...", Toast.LENGTH_SHORT).show()
//
// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
// .setAction("Action", null) //sert à rien
// .setAnchorView(R.id.fab).show() //au dessus du bouton mail
// Implémente la création d'une vidéo
}
fun createVideoRenderDialog(viewfortoast: View, projectId: Int, measurements: List<Measurement>) {
val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
val layout: View = inflater.inflate(R.layout.video_render_dialog_layout, findViewById(R.id.layout_root))
val videoName = layout.findViewById<EditText>(R.id.video_name_dialog)
//val videoResolution = layout.findViewById<EditText>(R.id.video_resolution_dialog)
val videoDuration = layout.findViewById<EditText>(R.id.video_duration_dialog)
val builder = AlertDialog.Builder(this)
builder.setView(layout)
.setPositiveButton("Render Video") { dialog, _ ->
val name = videoName.text.toString()
//val resolution = videoResolution.text.toString()
val durationText = videoDuration.text.toString()
if (name.isBlank() || durationText.isBlank()) {
Snackbar.make(viewfortoast, "All fields must be filled", Snackbar.LENGTH_LONG)
.show()
return@setPositiveButton
}
val duration = durationText.toIntOrNull()
if (duration == null || duration <= 0) {
Snackbar.make(viewfortoast, "Invalid duration", Snackbar.LENGTH_LONG)
.show()
return@setPositiveButton
}
val onSuccess: (Confirmation?) -> Unit = {
dialog.dismiss()
Snackbar.make(viewfortoast, "Video render started", Snackbar.LENGTH_LONG)
.show()
}
val onError: (String) -> Unit = { error ->
Snackbar.make(viewfortoast, "Error: $error", Snackbar.LENGTH_LONG)
.show()
}
videoRepository.createVideo(projectId, measures, name, "1920x1080", duration, onSuccess, onError)
}
.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
this.videoRenderDialog = builder.create()
}
fun createStartingProcedureDialog(viewfortoast: View, projectId: Int) {
val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
val layout: View = inflater.inflate(R.layout.capture_start_dialog_layout, findViewById(R.id.layout_root))
val intervalView = layout.findViewById<EditText>(R.id.interval)
//val videoResolution = layout.findViewById<EditText>(R.id.video_resolution_dialog)
val nbImView = layout.findViewById<EditText>(R.id.nb_images)
val builder = AlertDialog.Builder(this)
builder.setView(layout)
.setPositiveButton("Run capture procedure") { dialog, _ ->
val intervalStr = intervalView.text.toString()
//val resolution = videoResolution.text.toString()
val nb_imagesStr = nbImView.text.toString()
if (intervalStr.isBlank() || nb_imagesStr.isBlank()) {
Snackbar.make(viewfortoast, "Les champs doivent être remplis", Snackbar.LENGTH_LONG)
.show()
return@setPositiveButton
}
val interval = intervalStr.toIntOrNull()
val nb_images = nb_imagesStr.toIntOrNull()
if (interval == null || interval <= 0 || nb_images == null || nb_images <= 0) {
Snackbar.make(viewfortoast, "Les chiffres fournis doivent être superieurs à 1", Snackbar.LENGTH_LONG)
.show()
return@setPositiveButton
}
val onSuccess: () -> Unit = {
dialog.dismiss()
Snackbar.make(viewfortoast, "Processus de capture lancé", Snackbar.LENGTH_LONG)
.show()
buttonState = ButtonState.LOADING
refreshMultiuseButton()
}
val onError: (String) -> Unit = { error ->
Snackbar.make(viewfortoast, "Error : $error", Snackbar.LENGTH_LONG)
.show()
}
apiService.procedureStart(CaptureQuery(interval, nb_images, projectId)).enqueue(object :
Callback<Void> {
override fun onResponse(call: Call<Void>, response: Response<Void>) {
if (response.isSuccessful) {
refreshProjectInfo(onSuccess, onError)
} else {
onError("Erreur serveur : ${response.code()}")
}
}
override fun onFailure(call: Call<Void>, t: Throwable) {
onError("Erreur réseau : ${t.message}")
}
})
}
.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
this.startingProcedureDialog = builder.create()
}
fun createStopProcedureDialog(viewfortoast: View, projectId: Int) {
val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
val layout: View = inflater.inflate(R.layout.capture_stop_dialog_layout, findViewById(R.id.layout_root))
val builder = AlertDialog.Builder(this)
builder.setView(layout)
.setPositiveButton("Stop") { dialog, _ ->
val onSuccess: () -> Unit = {
dialog.dismiss()
Snackbar.make(viewfortoast, "Processus arreté", Snackbar.LENGTH_LONG)
.show()
buttonState = ButtonState.FINISHED
refreshMultiuseButton()
}
val onError: (String) -> Unit = { error ->
Snackbar.make(viewfortoast, "Error : $error", Snackbar.LENGTH_LONG)
.show()
}
apiService.procedureStop().enqueue(object :
Callback<Void> {
override fun onResponse(call: Call<Void>, response: Response<Void>) {
if (response.isSuccessful) {
refreshProjectInfo(onSuccess, onError)
} else {
onError("Erreur serveur : ${response.code()}")
}
}
override fun onFailure(call: Call<Void>, t: Throwable) {
onError("Erreur réseau : ${t.message}")
}
})
}
.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }
this.stopingProcedureDialog = builder.create()
}
fun refreshProjectInfo(cbGood: ()->Unit, cbBad: (String)->Unit){
this.project?.let {
projectRepository.fetchProject(it.id, onSuccess = {
Log.i("ProjectActivity", "Pull projet ok")
this.project = it;
this.initProjectInfo()
cbGood()
}, onError = {
Log.i("ProjectActivity", "Erreur de pull projet : $it")
cbBad("Erreur de pull projet : $it")
})
}
}
override fun onBackPressed() {
super.onBackPressed()
startActivity(Intent(this, MainActivity::class.java))
}
} }

View File

@@ -74,9 +74,10 @@ class ProjectAdapter(private val projects: MutableList<Project>, private val lis
} }
fun removeItem(position: Int, context: Context, v : View) { fun removeItem(position: Int, context: Context, v : View) {
val builder = AlertDialog.Builder(context) val builder = AlertDialog.Builder(context)
val dialog = builder.setTitle("Confirm Deletion") val dialog = builder.setTitle("Confirmer la supression")
.setMessage("Voulez vous supprimer ce projet ?") .setMessage("Voulez vous supprimer ce projet ?")
.setPositiveButton("Supprimer") { dialog, _ -> .setPositiveButton("Supprimer") { dialog, _ ->
if(projects[position].status != 3 && projects[position].status != 2){
projectRepository.deleteProject(projects[position].id, onSuccess = { projectRepository.deleteProject(projects[position].id, onSuccess = {
val name = projects[position].name val name = projects[position].name
projects.removeAt(position) projects.removeAt(position)
@@ -87,11 +88,14 @@ class ProjectAdapter(private val projects: MutableList<Project>, private val lis
notifyItemChanged(position) notifyItemChanged(position)
Snackbar.make(v.rootView, "Error while trying to delete ${projects[position].name} : $s", Snackbar.LENGTH_SHORT).show() Snackbar.make(v.rootView, "Error while trying to delete ${projects[position].name} : $s", Snackbar.LENGTH_SHORT).show()
}) })
}else{
notifyItemChanged(position)
Snackbar.make(v.rootView, "On ne peut pas supprimer un projet dans un état intermédiaire", Snackbar.LENGTH_SHORT).show()
}
} }
.setNegativeButton("Annuler") { dialog, _ -> .setNegativeButton("Annuler") { dialog, _ ->
dialog.dismiss() dialog.dismiss()
notifyItemChanged(position) // Reset swipe animation
} }
.show() .show()
dialog.setOnDismissListener { dialog.setOnDismissListener {

View File

@@ -17,6 +17,8 @@ interface ApiService {
fun getMeasurements(@Path("id") projectId: Int): Call<List<Measurement>> fun getMeasurements(@Path("id") projectId: Int): Call<List<Measurement>>
@GET("videos") @GET("videos")
fun getVideos(): Call<List<VideoDTO>> fun getVideos(): Call<List<VideoDTO>>
@POST("videos")
fun createVideo(@Body video: VideoDTO): Call<Confirmation>
@DELETE("videos/{id}") @DELETE("videos/{id}")
fun deleteVideo(@Path("id") videoId : Int): Call<Void> fun deleteVideo(@Path("id") videoId : Int): Call<Void>
@POST("projects/") @POST("projects/")
@@ -25,4 +27,8 @@ interface ApiService {
fun getVideosOfProject(@Path("id") projectId:Int): Call<List<VideoDTO>> fun getVideosOfProject(@Path("id") projectId:Int): Call<List<VideoDTO>>
@DELETE("projects/{id}") @DELETE("projects/{id}")
fun deleteProject(@Path("id") projectId:Int) : Call<Void> fun deleteProject(@Path("id") projectId:Int) : Call<Void>
@POST("procedure/start")
fun procedureStart(@Body captureQuery: CaptureQuery): Call<Void>
@POST("procedure/stop")
fun procedureStop(): Call<Void>
} }

View File

@@ -0,0 +1,8 @@
package com.dreamteam.timelapse.data
data class CaptureQuery (
val interval: Int,
val nb_images: Int,
val project_id: Int
)
{}

View File

@@ -24,7 +24,7 @@ data class Project(
fun getStatusText(): String{ fun getStatusText(): String{
Log.i("Project", "Status $status being trasnlated") Log.i("Project", "Status $status being trasnlated")
val statusArr = arrayOf("Brouillon", "En Cours", "Terminé", "Annulé") val statusArr = arrayOf("Brouillon", "En Cours", "Terminé", "En cours d'arret")
return statusArr[status] return statusArr[status]
} }
fun getStatusColor(): Int{ fun getStatusColor(): Int{

View File

@@ -56,7 +56,8 @@ class ProjectRepository(private val apiService: ApiService) {
onSuccess(it) onSuccess(it)
} ?: onError("Aucune mesure trouvé") } ?: onError("Aucune mesure trouvé")
} else { } else {
onError("Erreur : ${response.code()}") onSuccess(emptyList())
//onError("Erreur reception measures: ${response.code()}")
} }
} }
@@ -72,7 +73,7 @@ class ProjectRepository(private val apiService: ApiService) {
if (response.isSuccessful) { if (response.isSuccessful) {
onSuccess() onSuccess()
} else { } else {
onError("Erreur : ${response.code()}") onError("Erreur creation projet : ${response.code()}, ${response.body()}")
} }
} }

View File

@@ -1,5 +1,6 @@
package com.dreamteam.timelapse.data package com.dreamteam.timelapse.data
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@@ -16,7 +17,15 @@ data class VideoDTO (
fun toVideo(): Video { fun toVideo(): Video {
fun parseIntArray(jsonString: String): List<Int> { fun parseIntArray(jsonString: String): List<Int> {
val type = object : TypeToken<List<Int>>() {}.type val type = object : TypeToken<List<Int>>() {}.type
try {
Log.d("VideoDTO", Gson().fromJson<List<Int>>(jsonString, type).toString())
return Gson().fromJson<List<Int>>(jsonString, type) return Gson().fromJson<List<Int>>(jsonString, type)
}catch (e: Exception){
Log.d("VideoDTO", jsonString)
return Gson().fromJson<List<Int>>("[]", type)
//throw Exception("Tentative de transformer une videoDTO en video alors que le format est pas bon")
}
} }
return Video(id, project_id, parseIntArray(measurement_ids), video_file, resolution, duration, status, name) return Video(id, project_id, parseIntArray(measurement_ids), video_file, resolution, duration, status, name)
} }

View File

@@ -1,5 +1,6 @@
package com.dreamteam.timelapse.data package com.dreamteam.timelapse.data
import android.util.Log
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@@ -39,15 +40,20 @@ class VideoRepository(private val apiService: ApiService) {
} }
}) })
} }
fun fetchVideosOfProject(id:Int, onSuccess: (List<Video>) -> Unit, onError: (String) -> Unit){ fun fetchRenderedVideosOfProject(id:Int, onSuccess: (List<Video>) -> Unit, onError: (String) -> Unit){
apiService.getVideosOfProject(id).enqueue(object : Callback<List<VideoDTO>> { apiService.getVideosOfProject(id).enqueue(object : Callback<List<VideoDTO>> {
override fun onResponse(call: Call<List<VideoDTO>>, response: Response<List<VideoDTO>>) { override fun onResponse(call: Call<List<VideoDTO>>, response: Response<List<VideoDTO>>) {
if (response.isSuccessful) { if (response.isSuccessful) {
val videoDtos = response.body() ?: emptyList() val videoDtos = response.body() ?: emptyList()
videoDtos.forEach {
Log.d("VideoRepository", it.toString())
it.toVideo()
}
val videos = videoDtos.map { it.toVideo() } // Convert ugly data to clean Video objects val videos = videoDtos.map { it.toVideo() } // Convert ugly data to clean Video objects
onSuccess(videos) onSuccess(videos.filter { v -> v.video_file != null })
} else { } else {
onError("Erreur serveur : ${response.code()}") onSuccess(emptyList())
//onError("Erreur serveur : ${response.code()} ; id : $id ; ${response.body()}")
} }
} }
@@ -88,4 +94,24 @@ class VideoRepository(private val apiService: ApiService) {
onError("Erreur réseau : ${t.message}") onError("Erreur réseau : ${t.message}")
} }
}) })
}} }
fun createVideo(project_id:Int, measurements: List<Measurement>,
name:String, resolution: String, duration:Int,
onSuccess: (Confirmation?) -> Unit, onError: (String) -> Unit){
Log.d("VideoRepository", measurements.map {m -> m.order_id}.toString())
var v = VideoDTO(0, project_id, measurements.map {m -> m.order_id}.toString(), null, resolution, duration, 0, name)
apiService.createVideo(v).enqueue(object : Callback<Confirmation> {
override fun onResponse(call: Call<Confirmation>, response: Response<Confirmation>) {
if (response.isSuccessful) {
onSuccess(response.body())
} else {
onError("Erreur : ${response.code()} ; ${response.body()}")
}
}
override fun onFailure(call: Call<Confirmation>, t: Throwable) {
onError("Échec de l'appel API : ${t.message}")
}
})
}
}

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M6,6h12v12H6z"/>
</vector>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/interval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Frequence (min)"
android:inputType="number"/>
<!--<EditText
android:id="@+id/video_resolution_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Resolution (e.g., 1080p)" />-->
<EditText
android:id="@+id/nb_images"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nombre d'images"
android:inputType="number" />
</LinearLayout>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="25sp"
android:text="Voulez vous annuler la prise de vue ? (la demande sera prise en compte par la camera lors de son prochain redemarrage)" />
</LinearLayout>

View File

@@ -165,4 +165,15 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
</com.dreamteam.timelapse.CustomSwipeRefreshLayout> </com.dreamteam.timelapse.CustomSwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/multiusefloating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="25dp"
android:layout_marginBottom="25dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@android:drawable/ic_media_play" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/video_name_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Video Name" />
<!--<EditText
android:id="@+id/video_resolution_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Resolution (e.g., 1080p)" />-->
<EditText
android:id="@+id/video_duration_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Duration (seconds)"
android:inputType="number" />
</LinearLayout>