Bimba.git

commit a9949ce8c07eff6f6a8fa50a4279eb4aa74261dc

Author: Adam Evyčędo <git@apiote.xyz>

fix display of alerts and departures updates progress bar

%!v(PANIC=String method: strings: negative Repeat count)


diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
index 7b334c3d4efecdfa7b18e8d98c1de5bb9d8e1e6c..498c142721249c64f9bc154b7c78a7e3b5760618 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt
@@ -14,12 +14,14 @@ import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.Button
 import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.appcompat.widget.TooltipCompat
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import com.google.android.material.bottomsheet.BottomSheetDialog
 import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -33,15 +35,16 @@ import org.osmdroid.views.overlay.TilesOverlay
 import org.osmdroid.views.overlay.gestures.RotationGestureOverlay
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.dpToPixelI
+import xyz.apiote.bimba.czwek.repo.Alert
 import xyz.apiote.bimba.czwek.repo.CongestionLevel
 import xyz.apiote.bimba.czwek.repo.Departure
+import xyz.apiote.bimba.czwek.repo.DepartureItem
 import xyz.apiote.bimba.czwek.repo.OccupancyStatus
 import xyz.apiote.bimba.czwek.repo.Vehicle
 import java.time.ZoneId
 import java.time.ZonedDateTime
 
-
-class BimbaDepartureViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+class BimbaDepartureViewHolder(itemView: View) : ViewHolder(itemView) {
 	val root: View = itemView.findViewById(R.id.departure)
 	val lineIcon: ImageView = itemView.findViewById(R.id.line_icon)
 	val departureTime: TextView = itemView.findViewById(R.id.departure_time)
@@ -75,85 +78,151 @@ 		}
 	}
 }
 
+class BimbaAlertViewHolder(itemView: View) : ViewHolder(itemView) {
+	val root: View = itemView.findViewById(R.id.alerts)
+	val text: TextView = itemView.findViewById(R.id.alerts_text)
+	val moreButton: Button = itemView.findViewById(R.id.more_button)
+
+	companion object {
+		fun bind(
+			alerts: List<Alert>,
+			holder: BimbaAlertViewHolder?,
+			context: Context?
+		) {
+			val alertDescriptions = alerts.map { it.description }.filter { it != "" }
+				.joinToString(separator = "\n")
+			holder?.moreButton?.setOnClickListener{
+				MaterialAlertDialogBuilder(context!!)
+					.setTitle("Alerts")
+					.setPositiveButton(R.string.ok) { _, _ -> }
+					.setMessage(alertDescriptions)
+					.show()
+			}
+			holder?.moreButton?.visibility = if (alertDescriptions == "") View.GONE else View.VISIBLE
+			holder?.text?.text = alerts.map {
+				it.header.ifEmpty {
+					context!!.getString(R.string.alert_header)
+				}
+			}.toSet().joinToString(separator = "\n")
+		}
+	}
+}
+
 class BimbaDeparturesAdapter(
 	private val inflater: LayoutInflater,
 	private val context: Context?,
-	private var departures: List<Departure>,
+	private var items: List<DepartureItem>,
 	private val onClickListener: ((Departure) -> Unit)
 ) :
-	RecyclerView.Adapter<BimbaDepartureViewHolder>() {
+	RecyclerView.Adapter<ViewHolder>() {
 	var lastUpdate: ZonedDateTime =
 		ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault())
 		private set
 	private var showAsTime: Boolean = false
 
 	inner class DiffUtilCallback(
-		private val oldDepartures: List<Departure>,
-		private val newDepartures: List<Departure>,
-		private val showAsTimeChanged: Boolean
+		private val oldDepartures: List<DepartureItem>,
+		private val newDepartures: List<DepartureItem>,
+		private val showAsTimeChanged: Boolean,
 	) : DiffUtil.Callback() {
 		override fun getOldListSize() = oldDepartures.size
 
 		override fun getNewListSize() = newDepartures.size
 
 		override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
-			oldDepartures[oldItemPosition].ID == newDepartures[newItemPosition].ID
+			(oldDepartures[oldItemPosition].departure?.ID
+				?: "alert") == (newDepartures[newItemPosition].departure?.ID ?: "alert")
 
 		override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
 			val oldDeparture = oldDepartures[oldItemPosition]
 			val newDeparture = newDepartures[newItemPosition]
-			return oldDeparture.vehicle.Line == newDeparture.vehicle.Line && oldDeparture.vehicle.Headsign == newDeparture.vehicle.Headsign &&
-				oldDeparture.statusText(context, false, lastUpdate) == newDeparture.statusText(
-				context,
+			return if (oldDeparture.departure != null && newDeparture.departure != null) {
+				oldDeparture.departure.vehicle.Line == newDeparture.departure.vehicle.Line &&
+					oldDeparture.departure.vehicle.Headsign == newDeparture.departure.vehicle.Headsign &&
+					oldDeparture.departure.statusText(
+						context,
+						false,
+						lastUpdate
+					) == newDeparture.departure.statusText(context, false) && !showAsTimeChanged
+			} else if (oldDeparture.alert.isNotEmpty() && newDeparture.alert.isEmpty()) {
+				oldDeparture.alert == newDeparture.alert
+			} else {
 				false
-			)  && !showAsTimeChanged
+			}
 		}
 	}
 
 	private var departuresPositions: MutableMap<String, Int> = HashMap()
 
 	init {
-		departures.forEachIndexed { i, departure ->
-			departuresPositions[departure.ID] = i
+		items.forEachIndexed { i, departure ->
+			departuresPositions[departure.departure?.ID ?: "alert"] = i
 		}
 	}
 
-	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaDepartureViewHolder {
-		val rowView = inflater.inflate(R.layout.departure, parent, false)
-		return BimbaDepartureViewHolder(rowView)
+	override fun getItemViewType(position: Int): Int {
+		return if (items[position].departure != null) {
+			0
+		} else {
+			1
+		}
 	}
 
-	override fun onBindViewHolder(holder: BimbaDepartureViewHolder, position: Int) {
-		BimbaDepartureViewHolder.bind(
-			departures[position],
-			holder,
-			context,
-			showAsTime,
-			onClickListener
-		)
+	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+		if (viewType == 0) {
+			val rowView = inflater.inflate(R.layout.departure, parent, false)
+			return BimbaDepartureViewHolder(rowView)
+		} else {
+			val rowView = inflater.inflate(R.layout.alert, parent, false)
+			return BimbaAlertViewHolder(rowView)
+		}
+	}
+
+	override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+		if (holder is BimbaDepartureViewHolder) {
+			BimbaDepartureViewHolder.bind(
+				items[position].departure!!,
+				holder,
+				context,
+				showAsTime,
+				onClickListener
+			)
+		} else {
+			BimbaAlertViewHolder.bind(items[position].alert, holder as BimbaAlertViewHolder, context)
+		}
 	}
 
-	override fun getItemCount(): Int = departures.size
+	override fun getItemCount(): Int = items.size
 
-	fun get(id: String): Departure? {
+	fun get(id: String): DepartureItem? {
 		val position = departuresPositions[id]
 		return if (position == null) {
 			null
 		} else {
-			departures[position]
+			items[position]
 		}
 	}
 
-	fun update(departures: List<Departure>, showAsTime: Boolean, areNewObserved: Boolean = false) {
+	fun update(
+		departures: List<DepartureItem>,
+		showAsTime: Boolean,
+		areNewObserved: Boolean = false
+	) {
 		val newPositions: MutableMap<String, Int> = HashMap()
 		departures.forEachIndexed { i, departure ->
-			newPositions[departure.ID] = i
+			newPositions[departure.departure?.ID ?: "alert"] = i
 		}
-		val diff = DiffUtil.calculateDiff(DiffUtilCallback(this.departures, departures, this.showAsTime != showAsTime))
+		val diff = DiffUtil.calculateDiff(
+			DiffUtilCallback(
+				this.items,
+				departures,
+				this.showAsTime != showAsTime
+			)
+		)
 
 		this.showAsTime = showAsTime
 
-		this.departures = departures
+		this.items = departures
 		departuresPositions = newPositions
 		if (areNewObserved) {
 			lastUpdate = ZonedDateTime.now()
@@ -162,7 +231,7 @@ 		diff.dispatchUpdatesTo(this)
 	}
 
 	fun refreshItems() {
-		update(this.departures, showAsTime)
+		update(this.items, showAsTime)
 	}
 }
 




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
index 0f51af73cb0cdf09a9ec701a42a8192b6e230560..be0393f6b640339f4117d8582a6f3826c5027550 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt
@@ -16,10 +16,14 @@ import android.os.CountDownTimer
 import android.text.format.DateUtils
 import android.text.format.DateUtils.MINUTE_IN_MILLIS
 import android.view.View
+import androidx.activity.enableEdgeToEdge
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.core.content.res.ResourcesCompat
+import androidx.core.view.ViewCompat
 import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
 import androidx.lifecycle.ViewModelProvider
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -31,7 +35,7 @@ import com.google.android.material.timepicker.TimeFormat
 import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.api.Error
 import xyz.apiote.bimba.czwek.databinding.ActivityDeparturesBinding
-import xyz.apiote.bimba.czwek.repo.Departure
+import xyz.apiote.bimba.czwek.repo.DepartureItem
 import xyz.apiote.bimba.czwek.repo.Stop
 import xyz.apiote.bimba.czwek.units.Millisecond
 import xyz.apiote.bimba.czwek.units.Second
@@ -62,22 +66,36 @@ 	private val linesFilterTemporary = mutableMapOf()
 	private var alertDescriptions: String = ""
 
 	// TODO [elizabeth] millisInFuture from header Cache-Control max-age
-	private val countdown = object : CountDownTimer(Millisecond(Second(30)).millis, Millisecond(Tim(1)).millis) {
-		override fun onTick(millisUntilFinished: Long) {
-			val timsUntillFinished = Tim(Millisecond(millisUntilFinished))
-			binding.departuresUpdatesProgress.progress = timsUntillFinished.tims
-		}
+	private val countdown =
+		object : CountDownTimer(Millisecond(Second(30)).millis, Millisecond(Tim(1)).millis) {
+			override fun onTick(millisUntilFinished: Long) {
+				val timsUntillFinished = Tim(Millisecond(millisUntilFinished))
+				binding.departuresUpdatesProgress.progress = timsUntillFinished.tims
+			}
 
-		override fun onFinish() {
-			getDepartures()
+			override fun onFinish() {
+				getDepartures()
+			}
 		}
-	}
 
 	override fun onCreate(savedInstanceState: Bundle?) {
+		enableEdgeToEdge()
 		super.onCreate(savedInstanceState)
 		_binding = ActivityDeparturesBinding.inflate(layoutInflater)
 		setContentView(binding.root)
 
+		// TODO camera inset
+		ViewCompat.setOnApplyWindowInsetsListener(binding.departuresRecycler) { v, windowInsets ->
+			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+			v.updatePadding(bottom = insets.bottom)
+			WindowInsetsCompat.CONSUMED
+		}
+		ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets ->
+			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+			v.updatePadding(right = insets.right, left = insets.left)
+			windowInsets
+		}
+
 		viewModel = ViewModelProvider(this)[DeparturesViewModel::class.java]
 		getLine()?.let {
 			viewModel.linesFilter[it] = true
@@ -94,41 +112,23 @@ 				linesFilterTemporary.forEach { viewModel.linesFilter[it.key] = it.value }
 				getDepartures()
 			}
 
-		binding.moreButton.setOnClickListener {
-			MaterialAlertDialogBuilder(this)
-				.setTitle("Alerts")
-				.setPositiveButton(R.string.ok) { _, _ -> }
-				.setMessage(alertDescriptions)
-				.show()
-		}
-
 		viewModel.departures.observe(this) { stopDepartures ->
+			val items = mutableListOf<DepartureItem>()
 			if (stopDepartures.alerts.isNotEmpty()) {
-				binding.alerts.visibility = View.VISIBLE
-				binding.alertsText.text = stopDepartures.alerts.map {
-					it.header.ifEmpty {
-						getString(R.string.alert_header)
-					}
-				}.toSet().joinToString(separator = "\n")
-				alertDescriptions = stopDepartures.alerts.map { it.description }.filter { it != "" }
-					.joinToString(separator = "\n")
-				binding.moreButton.visibility = if (alertDescriptions == "") View.GONE else View.VISIBLE
-
-			} else {
-				binding.alerts.visibility = View.GONE
+				items.add(DepartureItem(stopDepartures.alerts))
 			}
-			updateItems(
-				stopDepartures.departures
-					.filter { d ->
-						viewModel.linesFilter.values.all { !it } or (viewModel.linesFilter[d.vehicle.Line.name] ?: false)
-					}
-					.filter { d ->
-						val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt())
-						t >= viewModel.startTime && t <= viewModel.endTime
-					},
-				stopDepartures.stop
-			)
-			viewModel.openBottomSheet?.departureID()?.let { adapter.get(it) }?.let { viewModel.openBottomSheet?.update(it) }
+			items.addAll(stopDepartures.departures
+				.filter { d ->
+					viewModel.linesFilter.values.all { !it } or (viewModel.linesFilter[d.vehicle.Line.name]
+						?: false)
+				}
+				.filter { d ->
+					val t = LocalTime.of(d.time.Hour.toInt(), d.time.Minute.toInt())
+					t >= viewModel.startTime && t <= viewModel.endTime
+				}.map { DepartureItem(it) })
+			updateItems(items, stopDepartures.stop)
+			viewModel.openBottomSheet?.departureID()?.let { adapter.get(it) }
+				?.let { it.departure?.let { departure -> viewModel.openBottomSheet?.update(departure) } }
 
 			val lines = stopDepartures.departures.map { it.vehicle.Line.name }.sortedWith { s1, s2 ->
 				val s1n = s1.toIntOrNull()
@@ -185,7 +185,9 @@ 				}
 
 				R.id.departures_filter_byline -> {
 					linesFilterTemporary.clear()
-					viewModel.linesFilter.forEach { filter -> linesFilterTemporary[filter.key] = filter.value }
+					viewModel.linesFilter.forEach { filter ->
+						linesFilterTemporary[filter.key] = filter.value
+					}
 					linePicker?.show()
 					true
 				}
@@ -245,10 +247,10 @@ 				}
 			}
 		)
 		adapter = BimbaDeparturesAdapter(layoutInflater, this, listOf()) {
-			DepartureBottomSheet(it).apply {
-				show(supportFragmentManager, DepartureBottomSheet.TAG)
-				viewModel.openBottomSheet = this
-				setOnCancel { viewModel.openBottomSheet = null }
+				DepartureBottomSheet(it).apply {
+					show(supportFragmentManager, DepartureBottomSheet.TAG)
+					viewModel.openBottomSheet = this
+					setOnCancel { viewModel.openBottomSheet = null }
 			}
 		}
 		binding.departuresRecycler.adapter = adapter
@@ -357,13 +359,11 @@
 	private fun showLoading() {
 		binding.departuresOverlay.visibility = View.VISIBLE
 		binding.departuresProgress.visibility = View.VISIBLE
-		binding.departuresRecycler.visibility = View.GONE
 		binding.errorImage.visibility = View.GONE
 		binding.errorText.visibility = View.GONE
-		binding.departuresUpdatesProgress.visibility = View.GONE
 	}
 
-	private fun updateItems(departures: List<Departure>, stop: Stop) {
+	private fun updateItems(departures: List<DepartureItem>, stop: Stop) {
 		setupSnackbar()
 		binding.departuresProgress.visibility = View.GONE
 		// TODO [elizabeth] max, progress from header Cache-Control max-age




diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
index 358a7bbd7ed88d19af4e2e9fdb6a26ff7774cb6f..e90ce512e3b7eba9e635c9514268ee246ddc9db8 100644
--- a/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
+++ b/app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt
@@ -22,6 +22,20 @@ import java.time.ZonedDateTime
 import java.time.format.DateTimeFormatter
 import java.time.temporal.ChronoUnit
 
+
+class DepartureItem {
+	private constructor(d: Departure?, a: List<Alert>) {
+		departure = d
+		alert = a
+	}
+
+	constructor(d: Departure) : this(d, emptyList())
+	constructor(a: List<Alert>) : this(null, a)
+
+	val departure: Departure?
+	val alert: List<Alert>
+}
+
 enum class AlertCause {
 	UNKNOWN, OTHER, TECHNICAL_PROBLEM, STRIKE, DEMONSTRATION, ACCIDENT, HOLIDAY, WEATHER, MAINTENANCE,
 	CONSTRUCTION, POLICE_ACTIVITY, MEDICAL_EMERGENCY;
@@ -61,7 +75,7 @@ 				AlertEffectV1.SIGNIFICANT_DELAYS -> valueOf("SIGNIFICANT_DELAYS")
 				AlertEffectV1.DETOUR -> valueOf("DETOUR")
 				AlertEffectV1.ADDITIONAL_SERVICE -> valueOf("ADDITIONAL_SERVICE")
 				AlertEffectV1.MODIFIED_SERVICE -> valueOf("MODIFIED_SERVICE")
-				AlertEffectV1.STOP_MOVED  -> valueOf("STOP_MOVED")
+				AlertEffectV1.STOP_MOVED -> valueOf("STOP_MOVED")
 				AlertEffectV1.NONE -> valueOf("NONE")
 				AlertEffectV1.ACCESSIBILITY_ISSUE -> valueOf("ACCESSIBILITY_ISSUE")
 			}
@@ -76,7 +90,13 @@ 	val url: String,
 	val cause: AlertCause,
 	val effect: AlertEffect
 ) {
-	constructor(a: AlertV1) : this(a.header, a.Description, a.Url, AlertCause.of(a.Cause), AlertEffect.of(a.Effect))
+	constructor(a: AlertV1) : this(
+		a.header,
+		a.Description,
+		a.Url,
+		AlertCause.of(a.Cause),
+		AlertEffect.of(a.Effect)
+	)
 }
 
 data class StopDepartures(




diff --git a/app/src/main/res/layout/activity_departures.xml b/app/src/main/res/layout/activity_departures.xml
index c5c97475c1c6fd8e75e36117e95b5c5d0de71570..d4361d70d4f149c2a311b179544e52b0c7994183 100644
--- a/app/src/main/res/layout/activity_departures.xml
+++ b/app/src/main/res/layout/activity_departures.xml
@@ -13,66 +13,51 @@ 	android:layout_width="match_parent"
 	android:layout_height="match_parent"
 	android:paddingBottom="16dp">
 
-	<androidx.recyclerview.widget.RecyclerView
-		android:id="@+id/departures_recycler"
+	<com.google.android.material.appbar.AppBarLayout
+		android:id="@+id/app_bar_layout"
 		android:layout_width="match_parent"
-		android:layout_height="match_parent"
-		android:clipToPadding="false"
-		android:fitsSystemWindows="true"
-		android:visibility="gone"
-		app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+		android:layout_height="wrap_content"
+		android:fitsSystemWindows="true">
 
-	<com.google.android.material.card.MaterialCardView
-		android:id="@+id/alerts"
-		android:layout_width="match_parent"
-		android:layout_height="100dp"
-		android:backgroundTint="@color/safety"
-		android:visibility="gone"
-		app:layout_anchor="@id/app_bar_layout"
-		app:layout_anchorGravity="bottom">
-
-		<androidx.constraintlayout.widget.ConstraintLayout
+		<com.google.android.material.appbar.CollapsingToolbarLayout
+			android:id="@+id/collapsing_layout"
+			style="?attr/collapsingToolbarLayoutMediumStyle"
 			android:layout_width="match_parent"
-			android:layout_height="match_parent">
+			android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
+			app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
+			app:maxLines="2">
 
-			<ImageView
-				android:id="@+id/imageView"
-				android:layout_width="wrap_content"
-				android:layout_height="wrap_content"
-				android:layout_marginStart="8dp"
-				android:importantForAccessibility="no"
-				android:src="@drawable/warning"
-				app:layout_constraintStart_toStartOf="parent"
-				app:layout_constraintTop_toTopOf="@+id/alerts_text"
-				app:tint="@color/black" />
+			<com.google.android.material.appbar.MaterialToolbar
+				android:id="@+id/departures_app_bar"
+				android:layout_width="match_parent"
+				android:layout_height="?attr/actionBarSize"
+				android:elevation="0dp"
+				app:layout_collapseMode="pin"
+				app:menu="@menu/departures_menu" />
 
-			<com.google.android.material.textview.MaterialTextView
-				android:id="@+id/alerts_text"
-				android:layout_width="0dp"
-				android:layout_height="0dp"
-				android:layout_marginStart="8dp"
-				android:layout_marginTop="58dp"
-				android:layout_marginEnd="8dp"
-				android:ellipsize="end"
-				android:maxLines="2"
-				android:textColor="@color/black"
-				app:layout_constraintBottom_toBottomOf="parent"
-				app:layout_constraintEnd_toStartOf="@+id/more_button"
-				app:layout_constraintStart_toEndOf="@+id/imageView"
-				app:layout_constraintTop_toTopOf="parent"
-				tool:text="Warning: Serious blockade on Piastowska towards Wojska Polskiego. Lines 5, 14, 163 diverted. Change for other means of transport, e.g. lines \n\naaaaa" />
+		</com.google.android.material.appbar.CollapsingToolbarLayout>
 
-			<com.google.android.material.button.MaterialButton
-				android:id="@+id/more_button"
-				style="@style/Widget.Material3.Button.TextButton"
-				android:layout_width="wrap_content"
-				android:layout_height="wrap_content"
-				android:text="@string/more"
-				android:textColor="@color/link"
-				app:layout_constraintBottom_toBottomOf="parent"
-				app:layout_constraintEnd_toEndOf="parent" />
-		</androidx.constraintlayout.widget.ConstraintLayout>
-	</com.google.android.material.card.MaterialCardView>
+		<com.google.android.material.progressindicator.LinearProgressIndicator
+			android:id="@+id/departures_updates_progress"
+			android:layout_width="match_parent"
+			android:layout_height="wrap_content"
+			android:layout_marginStart="4dp"
+			android:layout_marginEnd="4dp"
+			android:indeterminate="true"
+			android:visibility="gone" />
+
+	</com.google.android.material.appbar.AppBarLayout>
+
+	<androidx.recyclerview.widget.RecyclerView
+		android:id="@+id/departures_recycler"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		android:layout_marginTop="8dp"
+		android:clipToPadding="false"
+		app:layout_behavior="@string/appbar_scrolling_view_behavior"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@id/alerts" />
 
 	<androidx.constraintlayout.widget.ConstraintLayout
 		android:id="@+id/departures_overlay"
@@ -116,42 +101,5 @@ 			app:layout_constraintStart_toStartOf="parent"
 			app:layout_constraintTop_toBottomOf="@+id/error_image"
 			tool:text="No connection" />
 	</androidx.constraintlayout.widget.ConstraintLayout>
-
-	<com.google.android.material.appbar.AppBarLayout
-		android:id="@+id/app_bar_layout"
-		android:layout_width="match_parent"
-		android:layout_height="wrap_content"
-		android:fitsSystemWindows="true">
-
-		<com.google.android.material.appbar.CollapsingToolbarLayout
-			android:id="@+id/collapsing_layout"
-			style="?attr/collapsingToolbarLayoutMediumStyle"
-			android:layout_width="match_parent"
-			android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
-			app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
-			app:maxLines="2">
-
-			<com.google.android.material.appbar.MaterialToolbar
-				android:id="@+id/departures_app_bar"
-				android:layout_width="match_parent"
-				android:layout_height="?attr/actionBarSize"
-				android:elevation="0dp"
-				app:layout_collapseMode="pin"
-				app:menu="@menu/departures_menu" />
-
-		</com.google.android.material.appbar.CollapsingToolbarLayout>
-
-	</com.google.android.material.appbar.AppBarLayout>
-
-	<com.google.android.material.progressindicator.LinearProgressIndicator
-		android:layout_width="match_parent"
-		android:layout_height="wrap_content"
-		android:id="@+id/departures_updates_progress"
-		app:layout_anchor="@id/app_bar_layout"
-		app:layout_anchorGravity="bottom"
-		android:visibility="gone"
-		android:indeterminate="true"
-		android:layout_marginEnd="4dp"
-		android:layout_marginStart="4dp" />
 
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file




diff --git a/app/src/main/res/layout/alert.xml b/app/src/main/res/layout/alert.xml
new file mode 100644
index 0000000000000000000000000000000000000000..643fd32e34735a592742d869500910aa13314b87
--- /dev/null
+++ b/app/src/main/res/layout/alert.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
+	xmlns:tool="http://schemas.android.com/tools"
+	android:id="@+id/alerts"
+	android:layout_width="match_parent"
+	android:layout_height="64dp"
+	android:layout_marginStart="8dp"
+	android:layout_marginTop="8dp"
+	android:layout_marginEnd="8dp"
+	android:backgroundTint="@color/safety">
+
+	<androidx.constraintlayout.widget.ConstraintLayout
+		android:layout_width="match_parent"
+		android:layout_height="match_parent">
+
+		<ImageView
+			android:id="@+id/imageView"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_marginStart="8dp"
+			android:importantForAccessibility="no"
+			android:src="@drawable/warning"
+			app:layout_constraintStart_toStartOf="parent"
+			app:layout_constraintTop_toTopOf="@+id/alerts_text"
+			app:tint="@color/black" />
+
+		<com.google.android.material.textview.MaterialTextView
+			android:id="@+id/alerts_text"
+			tool:text="Warning: Serious blockade on Piastowska towards Wojska Polskiego. Lines 5, 14, 163 diverted. Change for other means of transport, e.g. lines \n\naaaaa"
+			android:layout_width="0dp"
+			android:layout_height="0dp"
+			android:layout_marginStart="8dp"
+			android:layout_marginTop="8dp"
+			android:layout_marginEnd="8dp"
+			android:ellipsize="end"
+			android:maxLines="2"
+			android:textColor="@color/black"
+			app:layout_constraintBottom_toBottomOf="parent"
+			app:layout_constraintEnd_toStartOf="@+id/more_button"
+			app:layout_constraintStart_toEndOf="@+id/imageView"
+			app:layout_constraintTop_toTopOf="parent" />
+
+		<com.google.android.material.button.MaterialButton
+			android:id="@+id/more_button"
+			style="@style/Widget.Material3.Button.TextButton"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:text="@string/more"
+			android:textColor="@color/link"
+			app:layout_constraintBottom_toBottomOf="parent"
+			app:layout_constraintEnd_toEndOf="parent" />
+	</androidx.constraintlayout.widget.ConstraintLayout>
+</com.google.android.material.card.MaterialCardView>
\ No newline at end of file