fix #5 #4 #8, en gros j'ai avancé les videos

This commit is contained in:
Raphael
2025-03-10 23:59:22 +01:00
parent 8ee061b791
commit 35b6120ebe
15 changed files with 401 additions and 44 deletions

View File

@@ -71,9 +71,10 @@ dependencies {
implementation("com.squareup.retrofit2:retrofit:2.9.0") //internet, api etc...
implementation("com.squareup.retrofit2:converter-gson:2.9.0") // Si tu veux utiliser Gson pour la sérialisation
implementation("com.squareup.okhttp3:logging-interceptor:4.9.0") // Pour le logging
implementation("com.github.bumptech.glide:glide:4.15.1") // Pour curl des images d'internet en gros
implementation("com.github.bumptech.glide:glide:4.15.1")
kapt("com.github.bumptech.glide:compiler:4.15.1")
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
implementation("com.google.android.exoplayer:exoplayer:2.18.1")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -21,10 +21,12 @@
android:theme="@style/Theme.Timelapse">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".VideoPlayerActivity" android:exported="false" android:label="@string/app_name" android:theme="@style/Theme.Timelapse">
</activity>
<activity
android:name=".ProjectActivity"
android:exported="true"

View File

@@ -6,12 +6,14 @@ import android.util.Log
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.dreamteam.timelapse.data.ApiService
import com.dreamteam.timelapse.data.Measurement
import com.dreamteam.timelapse.data.Project
import com.dreamteam.timelapse.data.ProjectRepository
import com.dreamteam.timelapse.data.VideoRepository
import com.dreamteam.timelapse.databinding.ProjectBinding
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
@@ -29,6 +31,7 @@ class ProjectActivity : AppCompatActivity() {
private lateinit var binding: ProjectBinding
private lateinit var projectRepository: ProjectRepository
private lateinit var videoRepository: VideoRepository
private lateinit var apiService: ApiService // Déclare l'apiService
private var measures: List<Measurement> = emptyList() // Déclare une liste de projets vide
private var project: Project? = null
@@ -50,7 +53,8 @@ class ProjectActivity : AppCompatActivity() {
.addConverterFactory(GsonConverterFactory.create())
.build()
apiService = retrofit.create(ApiService::class.java) // Crée une instance d'ApiService
projectRepository = ProjectRepository(apiService) // Tu initialises ton repository
projectRepository = ProjectRepository(apiService)
videoRepository = VideoRepository(apiService)
this.project = intent.getParcelableExtra<Project>("PROJECT") // -1 est la valeur par défaut si l'ID n'est pas trouvé
Log.d("project", "La project activity "+this.project?.id+" est créée")
@@ -67,6 +71,7 @@ class ProjectActivity : AppCompatActivity() {
fetchMeasuresAndRebuildGraph()
Log.d("ProjetsFrag", "Actualisation des projets")
}
fetchVideos()
@@ -80,7 +85,19 @@ class ProjectActivity : AppCompatActivity() {
// }
}
fun fetchVideos(){
this.project?.let{
videoRepository.fetchVideosOfProject(it.id, onSuccess = { videos ->
val adapter = VideoAdapter(false, videos, null) //ImageAdapter(imageUrls)
binding.videosList.layoutManager = GridLayoutManager(this.baseContext, 2, GridLayoutManager.HORIZONTAL, false)
binding.videosList.addItemDecoration(GridSpacingItemDecoration(16)) // 16px spacing
binding.videosList.adapter = adapter
}, onError = {errorMessage ->
Log.e("ProjectActivity", errorMessage)
fetchVideos()
})
}
}
fun fetchMeasuresAndRebuildGraph(){
this.project?.let{
projectRepository.fetchMeasurementsOfProject(it.id,
@@ -91,7 +108,7 @@ class ProjectActivity : AppCompatActivity() {
initGraph(this.measures)
val adapter = ImageAdapter(this.measures.map { m-> "https://timelapse.kerboul.me/api/images/${m.project_id}/${m.order_id}"}) //ImageAdapter(imageUrls)
binding.imagesList.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.HORIZONTAL)
binding.imagesList.layoutManager = GridLayoutManager(this.baseContext, 2, GridLayoutManager.HORIZONTAL, false)
binding.imagesList.addItemDecoration(GridSpacingItemDecoration(16)) // 16px spacing
binding.imagesList.adapter = adapter

View File

@@ -0,0 +1,86 @@
package com.dreamteam.timelapse
import android.annotation.SuppressLint
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.dreamteam.timelapse.data.ApiService
import com.dreamteam.timelapse.data.ProjectRepository
import com.dreamteam.timelapse.data.Video
import com.dreamteam.timelapse.data.VideoRepository
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class VideoAdapter(
public val withProjectNames : Boolean,
private val videos: List<Video>,
private val listener: OnEmptyStateListener?
) : RecyclerView.Adapter<VideoAdapter.VideoViewHolder>() {
private var projectRepository: ProjectRepository;
init {
val retrofit = Retrofit.Builder()
.baseUrl("https://timelapse.kerboul.me/api/")
.addConverterFactory(GsonConverterFactory.create())
.build()
projectRepository = ProjectRepository(retrofit.create(ApiService::class.java))
listener?.onEmptyStateChanged(videos.isEmpty())
}
interface OnEmptyStateListener {
fun onEmptyStateChanged(isEmpty: Boolean)
}
class VideoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val videoTitle: TextView = itemView.findViewById(R.id.videoTitle)
val videoThumbnail: ImageView = itemView.findViewById(R.id.videoThumbnail)
val playButton: ImageView = itemView.findViewById(R.id.playButton)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_video, parent, false)
return VideoViewHolder(view)
}
override fun onBindViewHolder(holder: VideoViewHolder, position: Int) {
val video = videos[position]
val context = holder.itemView.context
holder.videoTitle.text = video.name
if(withProjectNames){
projectRepository.fetchProject(video.project_id, { p->
holder.videoTitle.text = video.name + " (" + p.name + ")"
}, { s->
Log.e("VideoAdapter", s)
})
}
// Load video thumbnail
val url = "https://timelapse.kerboul.me/api/videos/file/${video.id}"
Glide.with(context)
.asBitmap()
.load(url) // Load the video URL
.frame(0)
.placeholder(R.drawable.not_found) // Optional placeholder
.into(holder.videoThumbnail)
// Click listener to open video player
holder.itemView.setOnClickListener {
val intent = Intent(context, VideoPlayerActivity::class.java)
intent.putExtra("VIDEO_URL", url) // Pass video URL
intent.setFlags(FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
override fun getItemCount(): Int {
return videos.size
}
}

View File

@@ -9,10 +9,11 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.dreamteam.timelapse.data.ProjectRepository
import com.dreamteam.timelapse.data.VideoRepository
import com.dreamteam.timelapse.databinding.FragmentProjetsBinding
import com.dreamteam.timelapse.data.ApiService
import com.dreamteam.timelapse.data.Project
import com.dreamteam.timelapse.data.Video
import com.dreamteam.timelapse.databinding.FragmentVideosBinding
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
@@ -20,17 +21,17 @@ import retrofit2.converter.gson.GsonConverterFactory
* A simple [Fragment] subclass as the default destination in the navigation.
*/
class VideoFrag : Fragment(), ProjectAdapter.OnEmptyStateListener {
class VideoFrag : Fragment(), VideoAdapter.OnEmptyStateListener {
private lateinit var projectRepository: ProjectRepository
private lateinit var videoRepository: VideoRepository
//private lateinit var listView: ListView
private lateinit var recyclerView: RecyclerView
private lateinit var projectAdapter: ProjectAdapter
private var _binding: FragmentProjetsBinding? = null
private lateinit var videoAdapter: VideoAdapter
private var _binding: FragmentVideosBinding? = null
private val binding get() = _binding!!
private lateinit var apiService: ApiService // Déclare l'apiService
private var projects: List<Project> = emptyList() // Déclare une liste de projets vide
private var videos: List<Video> = emptyList() // Déclare une liste de projets vide
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -47,11 +48,11 @@ class VideoFrag : Fragment(), ProjectAdapter.OnEmptyStateListener {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentProjetsBinding.inflate(inflater, container, false)
_binding = FragmentVideosBinding.inflate(inflater, container, false)
Log.d("ProjetsFrag", "Fragment Projets créé")
projectRepository = ProjectRepository(apiService) // Tu initialises ton repository
//fetchProjects()
videoRepository = VideoRepository(apiService) // Tu initialises ton repository
//fetchVideos()
return _binding?.root
}
@@ -62,10 +63,10 @@ class VideoFrag : Fragment(), ProjectAdapter.OnEmptyStateListener {
val swipeRefreshLayout = binding.swipeRefreshLayout
swipeRefreshLayout.setOnRefreshListener {
fetchProjects()
fetchVideos()
Log.d("ProjetsFrag", "Actualisation des projets")
}
fetchProjects()
fetchVideos()
recyclerView = binding.recyclerView // view.findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(context)
@@ -73,28 +74,28 @@ class VideoFrag : Fragment(), ProjectAdapter.OnEmptyStateListener {
}
private fun fetchProjects() {
private fun fetchVideos() {
// Simulez l'appel API
binding.swipeRefreshLayout.setEnabled(true)
binding.swipeRefreshLayout.setRefreshing(true)
//binding.swipeRefreshLayout.
//binding.swipeRefreshLayout.dragDown()
Log.d("ProjetsFrag", "User has refreshed the projects list")
projectRepository.fetchProjects(
onSuccess = { projects ->
Log.d("ProjetsFrag", "Projets reçus : $projects")
this.projects = projects
this.projectAdapter = ProjectAdapter(this.projects, this) //TODO : Potentiel problème pour les refresh ici (update de l'adapter ou un truc du genre à voir)
this.recyclerView.adapter = this.projectAdapter
Log.d("ProjetsFrag", "User has refreshed the videos list")
videoRepository.fetchVideos(
onSuccess = { videos ->
Log.d("ProjetsFrag", "Projets reçus : $videos")
this.videos = videos
this.videoAdapter = VideoAdapter(true, this.videos, this) //TODO : Potentiel problème pour les refresh ici (update de l'adapter ou un truc du genre à voir)
this.recyclerView.adapter = this.videoAdapter
// Mettre à jour le RecyclerView ou autre traitement
},
onError = { errorMessage ->
Log.e("ProjetsFrag prout", errorMessage)
//onEmptyStateChanged(true)
this.projects = listOf<Project>()
this.projectAdapter = ProjectAdapter(this.projects, this) //TODO : Potentiel problème pour les refresh ici (update de l'adapter ou un truc du genre à voir)
this.recyclerView.adapter = this.projectAdapter
this.videos = listOf<Video>()
this.videoAdapter = VideoAdapter(true, this.videos, this) //TODO : Potentiel problème pour les refresh ici (update de l'adapter ou un truc du genre à voir)
this.recyclerView.adapter = this.videoAdapter
}
)
@@ -103,9 +104,9 @@ class VideoFrag : Fragment(), ProjectAdapter.OnEmptyStateListener {
override fun onEmptyStateChanged(isEmpty: Boolean) {
// Afficher ou masquer le message
if (isEmpty) {
binding.noProjectsText.visibility = View.VISIBLE
binding.noVideosText.visibility = View.VISIBLE
} else {
binding.noProjectsText.visibility = View.GONE
binding.noVideosText.visibility = View.GONE
}
}

View File

@@ -0,0 +1,55 @@
package com.dreamteam.timelapse
import android.media.MediaPlayer
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.ui.PlayerView
class VideoPlayerActivity : AppCompatActivity() {
private lateinit var player: ExoPlayer
private lateinit var playerView: PlayerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_player)
// Get reference to the PlayerView
playerView = findViewById(R.id.playerView)
// Initialize the player
player = ExoPlayer.Builder(this).build()
// Get video URL from intent
val videoUrl = intent.getStringExtra("VIDEO_URL")
if (videoUrl.isNullOrEmpty()) {
Toast.makeText(this, "Invalid video URL", Toast.LENGTH_SHORT).show()
return
}
// Create a MediaItem from the URL
val mediaItem = MediaItem.fromUri(videoUrl)
// Set the media item to the player
player.setMediaItem(mediaItem)
// Prepare and start playback
player.prepare()
player.play()
// Connect the player to the PlayerView
playerView.player = player
}
override fun onPause() {
super.onPause()
player.pause() // Pause the player when the activity is paused
}
override fun onStop() {
super.onStop()
player.release() // Release the player when the activity is stopped
}
}

View File

@@ -14,6 +14,10 @@ interface ApiService {
fun getProjet(@Path("id") projectId: Int): Call<Project>
@GET("projects/{id}/measurements") // Remplace par l'endpoint de ton API
fun getMeasurements(@Path("id") projectId: Int): Call<List<Measurement>>
@GET("videos")
fun getVideos(): Call<List<VideoDTO>>
@POST("projects/")
fun createProject(@Body project: Project): Call<Confirmation>
@GET("projects/{id}/videos")
fun getVideosOfProject(@Path("id") projectId:Int): Call<List<VideoDTO>>
}

View File

@@ -26,6 +26,25 @@ class ProjectRepository(private val apiService: ApiService) {
}
})
}
fun fetchProject(id:Int, onSuccess: (Project) -> Unit, onError: (String) -> Unit) {
val call = apiService.getProjet(id)
call.enqueue(object : Callback<Project> {
override fun onResponse(call: Call<Project>, response: Response<Project>) {
if (response.isSuccessful) {
val project = response.body()
project?.let {
onSuccess(it)
} ?: onError("Le projet ${id} n'existe pas")
} else {
onError("Erreur : ${response.code()}")
}
}
override fun onFailure(call: Call<Project>, t: Throwable) {
onError("Échec de l'appel API : ${t.message}")
}
})
}
fun fetchMeasurementsOfProject(pid: Int, onSuccess: (List<Measurement>) -> Unit, onError: (String) -> Unit) {
val call = apiService.getMeasurements(pid)

View File

@@ -0,0 +1,24 @@
package com.dreamteam.timelapse.data
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
data class VideoDTO (
val id: Int,
val project_id: Int,
val measurement_ids: String,
val video_file: String?,
val resolution:String,
val duration: Int,
val status: Int, //0 pas fini, 1 fini
val name: String
){
fun toVideo(): Video {
fun parseIntArray(jsonString: String): List<Int> {
val type = object : TypeToken<List<Int>>() {}.type
return Gson().fromJson<List<Int>>(jsonString, type)
}
return Video(id, project_id, parseIntArray(measurement_ids), video_file, resolution, duration, status, name)
}
}

View File

@@ -0,0 +1,43 @@
package com.dreamteam.timelapse.data
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.Date
class VideoRepository(private val apiService: ApiService) {
fun fetchVideos(onSuccess: (List<Video>) -> Unit, onError: (String) -> Unit) {
apiService.getVideos().enqueue(object : Callback<List<VideoDTO>> {
override fun onResponse(call: Call<List<VideoDTO>>, response: Response<List<VideoDTO>>) {
if (response.isSuccessful) {
val videoDtos = response.body() ?: emptyList()
val videos = videoDtos.map { it.toVideo() } // Convert ugly data to clean Video objects
onSuccess(videos)
} else {
onError("Erreur serveur : ${response.code()}")
}
}
override fun onFailure(call: Call<List<VideoDTO>>, t: Throwable) {
onError("Erreur réseau : ${t.message}")
}
})
}
fun fetchVideosOfProject(id:Int, onSuccess: (List<Video>) -> Unit, onError: (String) -> Unit){
apiService.getVideosOfProject(id).enqueue(object : Callback<List<VideoDTO>> {
override fun onResponse(call: Call<List<VideoDTO>>, response: Response<List<VideoDTO>>) {
if (response.isSuccessful) {
val videoDtos = response.body() ?: emptyList()
val videos = videoDtos.map { it.toVideo() } // Convert ugly data to clean Video objects
onSuccess(videos)
} else {
onError("Erreur serveur : ${response.code()}")
}
}
override fun onFailure(call: Call<List<VideoDTO>>, t: Throwable) {
onError("Erreur réseau : ${t.message}")
}
})
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.exoplayer2.ui.PlayerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!--<SurfaceView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent" />-->

View File

@@ -1,28 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<!--<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".VideoFrag">
tools:context=".ProjetsFrag">
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<Button
android:id="@+id/button_second"
<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/previous"
app:layout_constraintBottom_toTopOf="@id/textview_second"
android:text="@string/next"
app:layout_constraintBottom_toTopOf="@id/textview_first"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textview_second"
android:id="@+id/textview_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
@@ -30,6 +30,45 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_second" />
app:layout_constraintTop_toBottomOf="@id/button_first" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
-->
<!--<androidx.swiperefreshlayout.widget.SwipeRefreshLayout-->
<com.dreamteam.timelapse.CustomSwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".VideoFrag"
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/noVideosText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Aucune video disponible"
android:visibility="gone"
android:gravity="center"
android:textSize="18sp"
android:layout_gravity="center" />
</FrameLayout>
<!--<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />-->
</com.dreamteam.timelapse.CustomSwipeRefreshLayout>
<!--</androidx.core.widget.NestedScrollView> -->

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!-- Video Thumbnail -->
<ImageView
android:id="@+id/videoThumbnail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/videoTitle"
/>
<!-- Play Button (Centered on Thumbnail) -->
<ImageView
android:id="@+id/playButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_media_play"
app:layout_constraintBottom_toBottomOf="@id/videoThumbnail"
app:layout_constraintEnd_toEndOf="@id/videoThumbnail"
app:layout_constraintStart_toStartOf="@id/videoThumbnail"
app:layout_constraintTop_toTopOf="@id/videoThumbnail" />
<!-- Video Title -->
<TextView
android:id="@+id/videoTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="..."
android:textAppearance="?android:attr/textAppearanceMedium"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@id/videoThumbnail"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:elevation="4dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -15,7 +15,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
tools:ignore="MissingConstraints">
tools:ignore="MissingConstraints"
app:layout_constraintEnd_toEndOf="parent"
>
<TextView
android:id="@+id/project_name"
@@ -35,13 +37,17 @@
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/appBarLayout"
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
tools:context=".ProjectActivity">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
tools:context=".ProjectActivity">
<androidx.constraintlayout.widget.ConstraintLayout
@@ -82,6 +88,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/project_start_date" />
<TextView
android:id="@+id/graph_header"
android:layout_width="wrap_content"
@@ -93,6 +100,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/project_status" />
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/temperature_humidity_chart"
android:layout_width="0dp"
@@ -147,8 +155,6 @@
app:layout_constraintTop_toBottomOf="@id/video_header" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</com.dreamteam.timelapse.CustomSwipeRefreshLayout>