Hoofdscherm: donker thema + wekkerkaarten met grote tijden (stijl systeemwekker), lege state met instructie (les uit Alarmio)

This commit is contained in:
eddy 2026-10-14 20:26:41 +02:00
commit d7ce319704
6 changed files with 212 additions and 120 deletions

View file

@ -5,39 +5,33 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.view.LayoutInflater
import android.os.Looper import android.view.View
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nl.eddydevink.buschauffeurswekker.core.AlarmScheduler import nl.eddydevink.buschauffeurswekker.core.AlarmScheduler
import nl.eddydevink.buschauffeurswekker.core.Prefs import nl.eddydevink.buschauffeurswekker.core.Prefs
import nl.eddydevink.buschauffeurswekker.core.ShiftCheck import nl.eddydevink.buschauffeurswekker.core.ShiftCheck
import nl.eddydevink.buschauffeurswekker.core.ShiftPlanner import nl.eddydevink.buschauffeurswekker.core.ShiftPlanner
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Locale
/** /**
* Hoofdscherm: klok + de geplande wekkers van morgen. Instellingen zitten in * Hoofdscherm in de stijl van de systeem-wekker-app: donker thema, één kaart
* [SettingsActivity]; de testwekker is alleen bereikbaar via launch-intent * per wekker met grote tijd, dag-label en naam. Acties zitten achter het ⋮-menu.
* (--ez test true), niet via de productie-UI.
*/ */
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var prefs: Prefs private lateinit var prefs: Prefs
private val handler = Handler(Looper.getMainLooper())
private val clockTick = object : Runnable {
override fun run() {
updateClock()
handler.postDelayed(this, 1000)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -46,10 +40,7 @@ class MainActivity : AppCompatActivity() {
prefs = Prefs(this) prefs = Prefs(this)
requestNotificationPermission() requestNotificationPermission()
findViewById<MaterialButton>(R.id.btn_check).setOnClickListener { runCheck() } findViewById<TextView>(R.id.tv_menu).setOnClickListener { showMenu(it) }
findViewById<MaterialButton>(R.id.btn_settings).setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}
AlarmScheduler.scheduleNoonCheck(this) AlarmScheduler.scheduleNoonCheck(this)
@ -92,56 +83,78 @@ class MainActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
renderStatus() renderStatus()
updateClock()
handler.removeCallbacks(clockTick)
handler.post(clockTick)
} }
override fun onPause() { private fun showMenu(anchor: View) {
handler.removeCallbacks(clockTick) val popup = PopupMenu(this, anchor)
super.onPause() popup.menu.add(0, MENU_CHECK, 0, "Nu controleren")
} popup.menu.add(0, MENU_SETTINGS, 1, "Instellingen")
popup.setOnMenuItemClickListener { item ->
private fun updateClock() { when (item.itemId) {
val now = LocalTime.now() MENU_CHECK -> runCheck()
findViewById<TextView>(R.id.tv_clock).text = HH_MM.format(now) MENU_SETTINGS -> startActivity(Intent(this, SettingsActivity::class.java))
findViewById<TextView>(R.id.tv_date).text = DATE.format(java.time.LocalDate.now()) }
true
}
popup.show()
} }
private fun renderStatus() { private fun renderStatus() {
val p = prefs val p = prefs
val (_, alarms) = p.loadPlan() val (day, alarms) = p.loadPlan()
val next = findViewById<TextView>(R.id.tv_next_alarms) val container = findViewById<LinearLayout>(R.id.alarm_container)
val info = findViewById<TextView>(R.id.tv_shift_info) container.removeAllViews()
val inflater = LayoutInflater.from(this)
if (alarms.isNotEmpty()) { if (alarms.isEmpty()) {
next.text = alarms.joinToString("\n") { "${it.name} ${HH_MM.format(it.time)}" } // Lege state, zoals Alarmio: vertel wat de gebruiker moet doen.
val shift = p.lastShiftStartIso?.let { runCatching { LocalDateTime.parse(it) }.getOrNull() } if (p.config() == null) {
info.text = if (shift != null) { container.addView(idleCard(inflater, "Geen instellingen", "Open het menu (⋮) → Instellingen en vul je agenda-gegevens in"))
val rounded = ShiftPlanner.roundDownToQuarter(shift.toLocalTime())
"Dienst om ${HH_MM.format(shift.toLocalTime())} · afgerond ${HH_MM.format(rounded)}"
} else { } else {
p.lastStatusTxt.orEmpty() container.addView(idleCard(inflater, "Geen wekkers gepland", "Morgen geen dienst"))
} }
} else { } else {
next.text = "Geen wekkers" val tomorrow = LocalDate.now(ZoneId.systemDefault()).plusDays(1)
info.text = p.lastStatusTxt ?: "Nog niet gecontroleerd" val dayLabel = when {
day == tomorrow -> "Morgen"
day != null -> "Gepland ${day.format(DAY_LABEL)}"
else -> "Gepland"
}
alarms.forEach { spec ->
container.addView(
inflater.inflate(R.layout.item_alarm_card, container, false).apply {
findViewById<TextView>(R.id.tv_day).text = dayLabel
findViewById<TextView>(R.id.tv_name).text = spec.name
findViewById<TextView>(R.id.tv_time).text = TIME.format(spec.time)
},
)
}
}
val info = findViewById<TextView>(R.id.tv_shift_info)
val shift = p.lastShiftStartIso?.let { runCatching { LocalDateTime.parse(it) }.getOrNull() }
info.text = if (shift != null) {
val rounded = ShiftPlanner.roundDownToQuarter(shift.toLocalTime())
"Dienst om ${TIME_SHORT.format(shift.toLocalTime())} · afgerond ${TIME_SHORT.format(rounded)}"
} else {
p.lastStatusTxt.orEmpty()
}
}
/** Grijze kaart voor lege toestanden (geen instellingen / geen dienst). */
private fun idleCard(inflater: LayoutInflater, title: String, sub: String): View {
return inflater.inflate(R.layout.item_idle_card, null, false).apply {
findViewById<TextView>(R.id.tv_title).text = title
findViewById<TextView>(R.id.tv_sub).text = sub
} }
} }
private fun runCheck() { private fun runCheck() {
val btn = findViewById<MaterialButton>(R.id.btn_check) toast("Bezig met controleren…")
btn.isEnabled = false
btn.text = "Bezig…"
lifecycleScope.launch { lifecycleScope.launch {
try { val r = ShiftCheck.run(this@MainActivity)
val result = ShiftCheck.run(this@MainActivity) renderStatus()
renderStatus() toast(r.message)
toast(result.message)
} finally {
btn.isEnabled = true
btn.text = "Nu controleren"
}
} }
} }
@ -158,11 +171,14 @@ class MainActivity : AppCompatActivity() {
private companion object { private companion object {
const val REQ_NOTIFICATIONS = 42 const val REQ_NOTIFICATIONS = 42
const val MENU_CHECK = 1
const val MENU_SETTINGS = 2
const val EXTRA_SETUP_URL = "url" const val EXTRA_SETUP_URL = "url"
const val EXTRA_SETUP_USER = "user" const val EXTRA_SETUP_USER = "user"
const val EXTRA_SETUP_PASS = "pass" const val EXTRA_SETUP_PASS = "pass"
const val EXTRA_TEST = "test" const val EXTRA_TEST = "test"
val HH_MM = DateTimeFormatter.ofPattern("HH:mm") val TIME = DateTimeFormatter.ofPattern("HH:mm")
val DATE = DateTimeFormatter.ofPattern("EEEE d MMMM") val TIME_SHORT = DateTimeFormatter.ofPattern("HH:mm")
val DAY_LABEL = DateTimeFormatter.ofPattern("EEE d MMM", Locale.getDefault())
} }
} }

View file

@ -2,69 +2,52 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center_horizontal" android:background="@color/bg"
android:orientation="vertical" android:orientation="vertical">
android:padding="24dp">
<TextView <LinearLayout
android:id="@+id/tv_clock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:textSize="72sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary" />
<View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="40dp"
android:layout_marginBottom="24dp"
android:background="?android:attr/listDivider" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="VOLGENDE WEKKERS" android:gravity="center_vertical"
android:textSize="12sp" android:orientation="horizontal"
android:textColor="?android:attr/textColorSecondary" /> android:paddingStart="24dp"
android:paddingEnd="8dp"
android:paddingTop="16dp">
<TextView <TextView
android:id="@+id/tv_next_alarms" android:id="@+id/tv_title"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Wekkers"
android:textColor="@color/text_bright"
android:textSize="34sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="Menu"
android:padding="12dp"
android:text="⋮"
android:textColor="@color/text_bright"
android:textSize="28sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/alarm_container"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:orientation="vertical" />
android:gravity="center"
android:textSize="22sp" />
<TextView <TextView
android:id="@+id/tv_shift_info" android:id="@+id/tv_shift_info"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginStart="24dp"
android:gravity="center" android:layout_marginTop="16dp"
android:textSize="14sp" android:layout_marginEnd="24dp"
android:textColor="?android:attr/textColorSecondary" /> android:textColor="@color/text_muted"
android:textSize="13sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_check"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="Nu controleren" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_settings"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Instellingen" />
</LinearLayout> </LinearLayout>

View file

@ -0,0 +1,50 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
app:cardBackgroundColor="@color/card_active"
app:cardCornerRadius="28dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_bright"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/text_muted"
android:textSize="15sp" />
</LinearLayout>
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@color/text_bright"
android:textSize="56sp"
android:textStyle="bold" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,35 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
app:cardBackgroundColor="@color/card_idle"
app:cardCornerRadius="28dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_bright"
android:textSize="17sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/text_muted"
android:textSize="14sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,6 +1,11 @@
<resources> <resources>
<color name="primary">#00695C</color> <color name="bg">#0A0A0C</color>
<color name="on_primary">#FFFFFF</color> <color name="card_active">#445C96</color>
<color name="secondary">#4DB6AC</color> <color name="card_idle">#1B1B1E</color>
<color name="ic_launcher_background">#00695C</color> <color name="text_bright">#EEF2FF</color>
<color name="text_muted">#A9B5D8</color>
<color name="primary">#A9B5D8</color>
<color name="on_primary">#0A0A0C</color>
<color name="secondary">#445C96</color>
<color name="ic_launcher_background">#445C96</color>
</resources> </resources>

View file

@ -1,12 +1,15 @@
<resources> <resources>
<style name="Theme.Buschauffeurswekker" parent="Theme.Material3.DayNight"> <style name="Theme.Buschauffeurswekker" parent="Theme.Material3.Dark">
<item name="colorPrimary">@color/primary</item> <item name="colorPrimary">@color/primary</item>
<item name="colorOnPrimary">@color/on_primary</item> <item name="colorOnPrimary">@color/on_primary</item>
<item name="colorSecondary">@color/secondary</item> <item name="colorSecondary">@color/secondary</item>
<item name="android:statusBarColor">@color/primary</item> <item name="colorOnSecondary">@color/text_bright</item>
<item name="android:windowBackground">@color/bg</item>
<item name="android:statusBarColor">@color/bg</item>
<item name="android:navigationBarColor">@color/bg</item>
</style> </style>
<style name="Theme.Buschauffeurswekker.Alarm" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.Buschauffeurswekker.Alarm" parent="Theme.Material3.Dark.NoActionBar">
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>
<item name="android:statusBarColor">@android:color/black</item> <item name="android:statusBarColor">@android:color/black</item>
<item name="android:navigationBarColor">@android:color/black</item> <item name="android:navigationBarColor">@android:color/black</item>