diff --git a/desktop/src/main/kotlin/SystemTray.kt b/desktop/src/main/kotlin/SystemTray.kt index 061e7a7f..85db9b78 100644 --- a/desktop/src/main/kotlin/SystemTray.kt +++ b/desktop/src/main/kotlin/SystemTray.kt @@ -4,8 +4,8 @@ import com.kdroid.composetray.tray.api.Tray import foliary.foliary.generated.resources.Res import foliary.foliary.generated.resources.app_name import foliary.foliary.generated.resources.ic_foliary_tray -import foliary.foliary.generated.resources.open_app -import foliary.foliary.generated.resources.quit +import foliary.foliary.generated.resources.system_tray_open_app +import foliary.foliary.generated.resources.system_tray_quit import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import java.awt.Window @@ -13,8 +13,8 @@ import java.awt.Window @Composable fun ApplicationScope.FoliaryTray(window: () -> Window) { val appName = stringResource(Res.string.app_name) - val openApp = stringResource(Res.string.open_app) - val quit = stringResource(Res.string.quit) + val openApp = stringResource(Res.string.system_tray_open_app) + val quit = stringResource(Res.string.system_tray_quit) Tray( icon = painterResource(Res.drawable.ic_foliary_tray), diff --git a/foliary/src/commonMain/composeResources/values-af/strings.xml b/foliary/src/commonMain/composeResources/values-af/strings.xml index 942e246f..4d70ede2 100644 --- a/foliary/src/commonMain/composeResources/values-af/strings.xml +++ b/foliary/src/commonMain/composeResources/values-af/strings.xml @@ -28,6 +28,6 @@ Teken uit Profiel Foliary - Maak Foliary oop - Verlaat + Maak Foliary oop + Verlaat diff --git a/foliary/src/commonMain/composeResources/values-ar/strings.xml b/foliary/src/commonMain/composeResources/values-ar/strings.xml index 8ee9b80f..2ccf5837 100644 --- a/foliary/src/commonMain/composeResources/values-ar/strings.xml +++ b/foliary/src/commonMain/composeResources/values-ar/strings.xml @@ -28,6 +28,6 @@ تسجيل الخروج الملف الشخصي فولياري - افتح فولياري - إنهاء + افتح فولياري + إنهاء diff --git a/foliary/src/commonMain/composeResources/values-az/strings.xml b/foliary/src/commonMain/composeResources/values-az/strings.xml index 72fc4559..99e4c0c0 100644 --- a/foliary/src/commonMain/composeResources/values-az/strings.xml +++ b/foliary/src/commonMain/composeResources/values-az/strings.xml @@ -28,6 +28,6 @@ Hesabdan çıx Profil Foliary - Foliary-ni aç - Çıx + Foliary-ni aç + Çıx diff --git a/foliary/src/commonMain/composeResources/values-bar/strings.xml b/foliary/src/commonMain/composeResources/values-bar/strings.xml index 617ddcc8..a34e5b71 100644 --- a/foliary/src/commonMain/composeResources/values-bar/strings.xml +++ b/foliary/src/commonMain/composeResources/values-bar/strings.xml @@ -28,6 +28,6 @@ Ausloggn Profil Foliary - Foliary öffna - Beendn + Foliary öffna + Beendn diff --git a/foliary/src/commonMain/composeResources/values-be/strings.xml b/foliary/src/commonMain/composeResources/values-be/strings.xml index 69ca4a4e..5ba04334 100644 --- a/foliary/src/commonMain/composeResources/values-be/strings.xml +++ b/foliary/src/commonMain/composeResources/values-be/strings.xml @@ -28,6 +28,6 @@ Выйсці Профіль Foliary - Адкрыць Foliary - Выхад + Адкрыць Foliary + Выхад diff --git a/foliary/src/commonMain/composeResources/values-bg/strings.xml b/foliary/src/commonMain/composeResources/values-bg/strings.xml index 584cff44..1b42efdb 100644 --- a/foliary/src/commonMain/composeResources/values-bg/strings.xml +++ b/foliary/src/commonMain/composeResources/values-bg/strings.xml @@ -28,6 +28,6 @@ Излизане Профил Foliary - Отвори Foliary - Изход + Отвори Foliary + Изход diff --git a/foliary/src/commonMain/composeResources/values-bn/strings.xml b/foliary/src/commonMain/composeResources/values-bn/strings.xml index 51937a42..5c469077 100644 --- a/foliary/src/commonMain/composeResources/values-bn/strings.xml +++ b/foliary/src/commonMain/composeResources/values-bn/strings.xml @@ -28,6 +28,6 @@ লগ আউট করুন প্রোফাইল ফোলিয়ারি - ফোলিয়ারি খুলুন - প্রস্থান করুন + ফোলিয়ারি খুলুন + প্রস্থান করুন diff --git a/foliary/src/commonMain/composeResources/values-ca/strings.xml b/foliary/src/commonMain/composeResources/values-ca/strings.xml index dd23c750..a6e92534 100644 --- a/foliary/src/commonMain/composeResources/values-ca/strings.xml +++ b/foliary/src/commonMain/composeResources/values-ca/strings.xml @@ -28,6 +28,6 @@ Tancar la sessió Perfil Foliary - Obrir Foliary - Sortir + Obrir Foliary + Sortir diff --git a/foliary/src/commonMain/composeResources/values-cs/strings.xml b/foliary/src/commonMain/composeResources/values-cs/strings.xml index b2dc3941..2b61a226 100644 --- a/foliary/src/commonMain/composeResources/values-cs/strings.xml +++ b/foliary/src/commonMain/composeResources/values-cs/strings.xml @@ -28,6 +28,6 @@ Odhlásit se Profil Foliary - Otevřít Foliary - Ukončit + Otevřít Foliary + Ukončit diff --git a/foliary/src/commonMain/composeResources/values-cy/strings.xml b/foliary/src/commonMain/composeResources/values-cy/strings.xml index a37a2ee2..cd5b3252 100644 --- a/foliary/src/commonMain/composeResources/values-cy/strings.xml +++ b/foliary/src/commonMain/composeResources/values-cy/strings.xml @@ -28,6 +28,6 @@ Allgofnodi Proffil Foliary - Agor Foliary - Gadael + Agor Foliary + Gadael diff --git a/foliary/src/commonMain/composeResources/values-da/strings.xml b/foliary/src/commonMain/composeResources/values-da/strings.xml index cd5e8792..809e2974 100644 --- a/foliary/src/commonMain/composeResources/values-da/strings.xml +++ b/foliary/src/commonMain/composeResources/values-da/strings.xml @@ -28,6 +28,6 @@ Log ud Profil Foliary - Åbn Foliary - Afslut + Åbn Foliary + Afslut diff --git a/foliary/src/commonMain/composeResources/values-de/strings.xml b/foliary/src/commonMain/composeResources/values-de/strings.xml index b4bcd51d..4a710ce2 100644 --- a/foliary/src/commonMain/composeResources/values-de/strings.xml +++ b/foliary/src/commonMain/composeResources/values-de/strings.xml @@ -28,6 +28,6 @@ Abmelden Profil Foliary - Foliary öffnen - Beenden + Foliary öffnen + Beenden diff --git a/foliary/src/commonMain/composeResources/values-el/strings.xml b/foliary/src/commonMain/composeResources/values-el/strings.xml index efd9042e..87cc1866 100644 --- a/foliary/src/commonMain/composeResources/values-el/strings.xml +++ b/foliary/src/commonMain/composeResources/values-el/strings.xml @@ -28,6 +28,6 @@ Αποσύνδεση Προφίλ Foliary - Άνοιγμα του Foliary - Έξοδος + Άνοιγμα του Foliary + Έξοδος diff --git a/foliary/src/commonMain/composeResources/values-en/strings.xml b/foliary/src/commonMain/composeResources/values-en/strings.xml index 04cc92b8..7a02bddf 100644 --- a/foliary/src/commonMain/composeResources/values-en/strings.xml +++ b/foliary/src/commonMain/composeResources/values-en/strings.xml @@ -65,9 +65,9 @@ Overdue - - Open Foliary - Quit + + Open Foliary + Quit diff --git a/foliary/src/commonMain/composeResources/values-es/strings.xml b/foliary/src/commonMain/composeResources/values-es/strings.xml index 6a915c8d..75c1c633 100644 --- a/foliary/src/commonMain/composeResources/values-es/strings.xml +++ b/foliary/src/commonMain/composeResources/values-es/strings.xml @@ -28,6 +28,6 @@ Cerrar sesión Perfil Foliary - Abrir Foliary - Salir + Abrir Foliary + Salir diff --git a/foliary/src/commonMain/composeResources/values-et/strings.xml b/foliary/src/commonMain/composeResources/values-et/strings.xml index 820491e7..303c1d6a 100644 --- a/foliary/src/commonMain/composeResources/values-et/strings.xml +++ b/foliary/src/commonMain/composeResources/values-et/strings.xml @@ -28,6 +28,6 @@ Logi välja Profiil Foliary - Ava Foliary - Välju + Ava Foliary + Välju diff --git a/foliary/src/commonMain/composeResources/values-eu/strings.xml b/foliary/src/commonMain/composeResources/values-eu/strings.xml index 92129476..2dea3fb3 100644 --- a/foliary/src/commonMain/composeResources/values-eu/strings.xml +++ b/foliary/src/commonMain/composeResources/values-eu/strings.xml @@ -28,6 +28,6 @@ Saioa itxi Profila Foliary - Ireki Foliary - Irten + Ireki Foliary + Irten diff --git a/foliary/src/commonMain/composeResources/values-fa/strings.xml b/foliary/src/commonMain/composeResources/values-fa/strings.xml index 68869a36..1caec81f 100644 --- a/foliary/src/commonMain/composeResources/values-fa/strings.xml +++ b/foliary/src/commonMain/composeResources/values-fa/strings.xml @@ -28,6 +28,6 @@ خروج از حساب پروفایل فولیاری - باز کردن فولیاری - خروج + باز کردن فولیاری + خروج diff --git a/foliary/src/commonMain/composeResources/values-fi/strings.xml b/foliary/src/commonMain/composeResources/values-fi/strings.xml index 092f168f..51d6686a 100644 --- a/foliary/src/commonMain/composeResources/values-fi/strings.xml +++ b/foliary/src/commonMain/composeResources/values-fi/strings.xml @@ -28,6 +28,6 @@ Kirjaudu ulos Profiili Foliary - Avaa Foliary - Lopeta + Avaa Foliary + Lopeta diff --git a/foliary/src/commonMain/composeResources/values-fil/strings.xml b/foliary/src/commonMain/composeResources/values-fil/strings.xml index be61600e..fc20d5d3 100644 --- a/foliary/src/commonMain/composeResources/values-fil/strings.xml +++ b/foliary/src/commonMain/composeResources/values-fil/strings.xml @@ -28,6 +28,6 @@ Mag-log out Profile Foliary - Buksan ang Foliary - Lumabas + Buksan ang Foliary + Lumabas diff --git a/foliary/src/commonMain/composeResources/values-fr/strings.xml b/foliary/src/commonMain/composeResources/values-fr/strings.xml index 90530cc7..093904f3 100644 --- a/foliary/src/commonMain/composeResources/values-fr/strings.xml +++ b/foliary/src/commonMain/composeResources/values-fr/strings.xml @@ -28,6 +28,6 @@ Se déconnecter Profil Foliary - Ouvrir Foliary - Quitter + Ouvrir Foliary + Quitter diff --git a/foliary/src/commonMain/composeResources/values-ga/strings.xml b/foliary/src/commonMain/composeResources/values-ga/strings.xml index 8fb2a5fe..761501f5 100644 --- a/foliary/src/commonMain/composeResources/values-ga/strings.xml +++ b/foliary/src/commonMain/composeResources/values-ga/strings.xml @@ -28,6 +28,6 @@ Logáil amach Próifíl Foliary - Oscail Foliary - Scoir + Oscail Foliary + Scoir diff --git a/foliary/src/commonMain/composeResources/values-gl/strings.xml b/foliary/src/commonMain/composeResources/values-gl/strings.xml index 4c842a2f..9406cf5f 100644 --- a/foliary/src/commonMain/composeResources/values-gl/strings.xml +++ b/foliary/src/commonMain/composeResources/values-gl/strings.xml @@ -28,6 +28,6 @@ Pechar sesión Perfil Foliary - Abrir Foliary - Saír + Abrir Foliary + Saír diff --git a/foliary/src/commonMain/composeResources/values-he/strings.xml b/foliary/src/commonMain/composeResources/values-he/strings.xml index 8541c2dc..37ed2e9a 100644 --- a/foliary/src/commonMain/composeResources/values-he/strings.xml +++ b/foliary/src/commonMain/composeResources/values-he/strings.xml @@ -28,6 +28,6 @@ התנתקות פרופיל Foliary - פתיחת Foliary - יציאה + פתיחת Foliary + יציאה diff --git a/foliary/src/commonMain/composeResources/values-hi/strings.xml b/foliary/src/commonMain/composeResources/values-hi/strings.xml index 3eca2813..250b313e 100644 --- a/foliary/src/commonMain/composeResources/values-hi/strings.xml +++ b/foliary/src/commonMain/composeResources/values-hi/strings.xml @@ -28,6 +28,6 @@ लॉग आउट करें प्रोफ़ाइल फोलियरी - फोलियरी खोलें - बाहर निकलें + फोलियरी खोलें + बाहर निकलें diff --git a/foliary/src/commonMain/composeResources/values-hr/strings.xml b/foliary/src/commonMain/composeResources/values-hr/strings.xml index 3f4228c3..c9d4344b 100644 --- a/foliary/src/commonMain/composeResources/values-hr/strings.xml +++ b/foliary/src/commonMain/composeResources/values-hr/strings.xml @@ -28,6 +28,6 @@ Odjava Profil Foliary - Otvori Foliary - Izađi + Otvori Foliary + Izađi diff --git a/foliary/src/commonMain/composeResources/values-hu/strings.xml b/foliary/src/commonMain/composeResources/values-hu/strings.xml index a50ac37d..06414628 100644 --- a/foliary/src/commonMain/composeResources/values-hu/strings.xml +++ b/foliary/src/commonMain/composeResources/values-hu/strings.xml @@ -28,6 +28,6 @@ Kijelentkezés Profil Foliary - Foliary megnyitása - Kilépés + Foliary megnyitása + Kilépés diff --git a/foliary/src/commonMain/composeResources/values-id/strings.xml b/foliary/src/commonMain/composeResources/values-id/strings.xml index d8d7c14f..3e783408 100644 --- a/foliary/src/commonMain/composeResources/values-id/strings.xml +++ b/foliary/src/commonMain/composeResources/values-id/strings.xml @@ -28,6 +28,6 @@ Keluar Profil Foliary - Buka Foliary - Keluar + Buka Foliary + Keluar diff --git a/foliary/src/commonMain/composeResources/values-it/strings.xml b/foliary/src/commonMain/composeResources/values-it/strings.xml index cc8528d3..1c8e4d2f 100644 --- a/foliary/src/commonMain/composeResources/values-it/strings.xml +++ b/foliary/src/commonMain/composeResources/values-it/strings.xml @@ -28,6 +28,6 @@ Disconnetti Profilo Foliary - Apri Foliary - Esci + Apri Foliary + Esci diff --git a/foliary/src/commonMain/composeResources/values-ja/strings.xml b/foliary/src/commonMain/composeResources/values-ja/strings.xml index 06bae8d1..29518207 100644 --- a/foliary/src/commonMain/composeResources/values-ja/strings.xml +++ b/foliary/src/commonMain/composeResources/values-ja/strings.xml @@ -28,6 +28,6 @@ ログアウト プロフィール Foliary - Foliaryを開く - 終了 + Foliaryを開く + 終了 diff --git a/foliary/src/commonMain/composeResources/values-kk/strings.xml b/foliary/src/commonMain/composeResources/values-kk/strings.xml index def3cf9a..477ad49c 100644 --- a/foliary/src/commonMain/composeResources/values-kk/strings.xml +++ b/foliary/src/commonMain/composeResources/values-kk/strings.xml @@ -28,6 +28,6 @@ Жүйеден шығу Профиль Foliary - Foliary-ді ашу - Шығу + Foliary-ді ашу + Шығу diff --git a/foliary/src/commonMain/composeResources/values-km/strings.xml b/foliary/src/commonMain/composeResources/values-km/strings.xml index 9f5b592f..686b8069 100644 --- a/foliary/src/commonMain/composeResources/values-km/strings.xml +++ b/foliary/src/commonMain/composeResources/values-km/strings.xml @@ -28,6 +28,6 @@ ចាកចេញ ប្រវត្តិរូប Foliary - បើក Foliary - ចាកចេញ + បើក Foliary + ចាកចេញ diff --git a/foliary/src/commonMain/composeResources/values-ko/strings.xml b/foliary/src/commonMain/composeResources/values-ko/strings.xml index 129880ba..b141951e 100644 --- a/foliary/src/commonMain/composeResources/values-ko/strings.xml +++ b/foliary/src/commonMain/composeResources/values-ko/strings.xml @@ -28,6 +28,6 @@ 로그아웃 프로필 Foliary - Foliary 열기 - 종료 + Foliary 열기 + 종료 diff --git a/foliary/src/commonMain/composeResources/values-lt/strings.xml b/foliary/src/commonMain/composeResources/values-lt/strings.xml index b01f60a4..b1e8f8f7 100644 --- a/foliary/src/commonMain/composeResources/values-lt/strings.xml +++ b/foliary/src/commonMain/composeResources/values-lt/strings.xml @@ -28,6 +28,6 @@ Atsijungti Profilis Foliary - Atidaryti „Foliary“ - Išeiti + Atidaryti „Foliary“ + Išeiti diff --git a/foliary/src/commonMain/composeResources/values-lv/strings.xml b/foliary/src/commonMain/composeResources/values-lv/strings.xml index b0ff8dba..851dbeb4 100644 --- a/foliary/src/commonMain/composeResources/values-lv/strings.xml +++ b/foliary/src/commonMain/composeResources/values-lv/strings.xml @@ -28,6 +28,6 @@ Izrakstīties Profils Foliary - Atvērt Foliary - Iziet + Atvērt Foliary + Iziet diff --git a/foliary/src/commonMain/composeResources/values-ms/strings.xml b/foliary/src/commonMain/composeResources/values-ms/strings.xml index 671feb5f..dca79ef0 100644 --- a/foliary/src/commonMain/composeResources/values-ms/strings.xml +++ b/foliary/src/commonMain/composeResources/values-ms/strings.xml @@ -28,6 +28,6 @@ Log keluar Profil Foliary - Buka Foliary - Keluar + Buka Foliary + Keluar diff --git a/foliary/src/commonMain/composeResources/values-mt/strings.xml b/foliary/src/commonMain/composeResources/values-mt/strings.xml index 0153f461..f7683db6 100644 --- a/foliary/src/commonMain/composeResources/values-mt/strings.xml +++ b/foliary/src/commonMain/composeResources/values-mt/strings.xml @@ -28,6 +28,6 @@ Oħroġ mill-kont Profil Foliary - Iftaħ Foliary - Oħroġ + Iftaħ Foliary + Oħroġ diff --git a/foliary/src/commonMain/composeResources/values-nap/strings.xml b/foliary/src/commonMain/composeResources/values-nap/strings.xml index 4b3abb88..3f079919 100644 --- a/foliary/src/commonMain/composeResources/values-nap/strings.xml +++ b/foliary/src/commonMain/composeResources/values-nap/strings.xml @@ -28,6 +28,6 @@ Scollegat\'te Prufilo Foliary - Apre Foliary - Jesce + Apre Foliary + Jesce diff --git a/foliary/src/commonMain/composeResources/values-nl/strings.xml b/foliary/src/commonMain/composeResources/values-nl/strings.xml index 58f12ffd..60321c9f 100644 --- a/foliary/src/commonMain/composeResources/values-nl/strings.xml +++ b/foliary/src/commonMain/composeResources/values-nl/strings.xml @@ -28,6 +28,6 @@ Uitloggen Profiel Foliary - Foliary openen - Afsluiten + Foliary openen + Afsluiten diff --git a/foliary/src/commonMain/composeResources/values-no/strings.xml b/foliary/src/commonMain/composeResources/values-no/strings.xml index 0c5b2051..c71ec28a 100644 --- a/foliary/src/commonMain/composeResources/values-no/strings.xml +++ b/foliary/src/commonMain/composeResources/values-no/strings.xml @@ -28,6 +28,6 @@ Logg ut Profil Foliary - Åpne Foliary - Avslutt + Åpne Foliary + Avslutt diff --git a/foliary/src/commonMain/composeResources/values-pa/strings.xml b/foliary/src/commonMain/composeResources/values-pa/strings.xml index 58b4edbd..9a30c697 100644 --- a/foliary/src/commonMain/composeResources/values-pa/strings.xml +++ b/foliary/src/commonMain/composeResources/values-pa/strings.xml @@ -28,6 +28,6 @@ ਲੌਗ ਆਊਟ ਪ੍ਰੋਫ਼ਾਈਲ ਫੋਲੀਅਰੀ - ਫੋਲੀਅਰੀ ਖੋਲ੍ਹੋ - ਬੰਦ ਕਰੋ + ਫੋਲੀਅਰੀ ਖੋਲ੍ਹੋ + ਬੰਦ ਕਰੋ diff --git a/foliary/src/commonMain/composeResources/values-pl/strings.xml b/foliary/src/commonMain/composeResources/values-pl/strings.xml index 6d7d8868..90ed09d0 100644 --- a/foliary/src/commonMain/composeResources/values-pl/strings.xml +++ b/foliary/src/commonMain/composeResources/values-pl/strings.xml @@ -28,6 +28,6 @@ Wyloguj się Profil Foliary - Otwórz Foliary - Zakończ + Otwórz Foliary + Zakończ diff --git a/foliary/src/commonMain/composeResources/values/strings.xml b/foliary/src/commonMain/composeResources/values/strings.xml index 04cc92b8..7f6e097d 100644 --- a/foliary/src/commonMain/composeResources/values/strings.xml +++ b/foliary/src/commonMain/composeResources/values/strings.xml @@ -1,6 +1,8 @@ Foliary + Confirm + Cancel @@ -59,15 +61,18 @@ Add description... Save Title is required + Due date + Add due date + Clear due date Overdue - - Open Foliary - Quit + + Open Foliary + Quit diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/provider/time/TimeProvider.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/provider/time/TimeProvider.kt index e8023215..8e3ddf0e 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/provider/time/TimeProvider.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/provider/time/TimeProvider.kt @@ -1,8 +1,11 @@ package dev.appoutlet.foliary.core.provider.time +import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.atTime +import kotlinx.datetime.format +import kotlinx.datetime.format.MonthNames import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import org.koin.core.annotation.Single @@ -10,20 +13,50 @@ import kotlin.time.Clock import kotlin.time.Duration.Companion.days import kotlin.time.Instant +private val StartOfDay = LocalTime(0, 0) private val LastNanosecondOfTheDay = LocalTime.fromNanosecondOfDay((1.days.inWholeNanoseconds - 1)) interface TimeProvider { fun now(): Instant + fun startOfToday(timeZone: TimeZone = TimeZone.currentSystemDefault()): Instant fun endOfToday(timeZone: TimeZone = TimeZone.currentSystemDefault()): Instant + fun endOfDay(instant: Instant, timeZone: TimeZone = TimeZone.currentSystemDefault()): Instant + fun displayText(instant: Instant, timeZone: TimeZone = TimeZone.currentSystemDefault()): String } @Single class DefaultTimeProvider(private val clock: Clock = Clock.System) : TimeProvider { + private val dateFormat = LocalDate.Format { + day() + chars(" ") + monthName(MonthNames.ENGLISH_ABBREVIATED) + chars(" ") + year() + } + override fun now(): Instant = clock.now() - override fun endOfToday(timeZone: TimeZone): Instant { + override fun startOfToday(timeZone: TimeZone): Instant { return now().toLocalDateTime(timeZone).date + .atTime(StartOfDay) + .toInstant(timeZone) + } + + override fun endOfDay( + instant: Instant, + timeZone: TimeZone + ): Instant { + return instant.toLocalDateTime(timeZone).date .atTime(LastNanosecondOfTheDay) .toInstant(timeZone) } + + override fun endOfToday(timeZone: TimeZone) = endOfDay(now(), timeZone) + + override fun displayText( + instant: Instant, + timeZone: TimeZone + ): String { + return instant.toLocalDateTime(timeZone).date.format(dateFormat) + } } diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePicker.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePicker.kt new file mode 100644 index 00000000..3bc002a5 --- /dev/null +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePicker.kt @@ -0,0 +1,195 @@ +package dev.appoutlet.foliary.core.ui.component.datepicker + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.composables.icons.lucide.Lucide +import com.composables.icons.lucide.X +import dev.appoutlet.foliary.core.ui.component.button.FoliarySecondaryButton +import dev.appoutlet.foliary.core.ui.component.card.FoliaryCard +import foliary.foliary.generated.resources.Res +import foliary.foliary.generated.resources.create_task_due_date_clear_a11y +import foliary.foliary.generated.resources.general_cancel +import foliary.foliary.generated.resources.general_confirm +import org.jetbrains.compose.resources.stringResource +import kotlin.time.Instant + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FoliaryDatePicker( + label: String, + placeholder: String, + selectedDate: Long?, + selectedDateDisplayText: String?, + onDateSelected: (Long?) -> Unit, + modifier: Modifier = Modifier, + minDateMillis: Long = Instant.DISTANT_PAST.toEpochMilliseconds(), + maxDateMillis: Long = Instant.DISTANT_FUTURE.toEpochMilliseconds(), +) { + var showBottomSheet by rememberSaveable { mutableStateOf(false) } + + Box(modifier = modifier) { + DatePickerCard( + label = label, + placeholder = placeholder, + selectedDateDisplayText = selectedDateDisplayText, + onClick = { showBottomSheet = true }, + onClear = { onDateSelected(null) } + ) + } + + if (showBottomSheet) { + DatePickerBottomSheet( + selectedDateMillis = selectedDate, + minDateMillis = minDateMillis, + maxDateMillis = maxDateMillis, + onDismiss = { showBottomSheet = false }, + onConfirm = { onDateSelected(it) } + ) + } +} + +@Composable +private fun DatePickerCard( + label: String, + placeholder: String, + selectedDateDisplayText: String?, + onClick: () -> Unit, + onClear: () -> Unit, +) { + val dateTextColor by animateColorAsState( + targetValue = if (selectedDateDisplayText != null) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onBackground + } + ) + + FoliaryCard(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .testTag("FoliaryDatePicker:Card") + .clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.padding(16.dp).weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + modifier = Modifier.testTag("FoliaryDatePicker:Value"), + text = selectedDateDisplayText ?: placeholder, + style = MaterialTheme.typography.bodyMedium, + color = dateTextColor + ) + } + + if (selectedDateDisplayText != null) { + DateClearButton( + modifier = Modifier.padding(end = 16.dp), + onClick = onClear, + ) + } + } + } +} + +@Composable +private fun DateClearButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier.testTag("FoliaryDatePicker:ClearButton"), + onClick = onClick, + ) { + Icon( + imageVector = Lucide.X, + contentDescription = stringResource(Res.string.create_task_due_date_clear_a11y), + tint = MaterialTheme.colorScheme.onSurface + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DatePickerBottomSheet( + selectedDateMillis: Long?, + minDateMillis: Long, + maxDateMillis: Long, + onDismiss: () -> Unit, + onConfirm: (Long) -> Unit, +) { + val datePickerState = rememberDatePickerState( + initialSelectedDateMillis = selectedDateMillis, + selectableDates = MinMaxSelectableDates(minDateMillis, maxDateMillis) + ) + + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.background + ) { + DatePicker( + state = datePickerState, + colors = DatePickerDefaults.colors( + containerColor = MaterialTheme.colorScheme.background, + ) + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End) + ) { + TextButton(onClick = onDismiss) { + Text(text = stringResource(Res.string.general_cancel)) + } + + FoliarySecondaryButton( + onClick = { + datePickerState.selectedDateMillis?.let(onConfirm) + onDismiss() + }, + enabled = datePickerState.selectedDateMillis != null + ) { + Text(text = stringResource(Res.string.general_confirm)) + } + } + } +} diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/MinMaxSelectableDates.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/MinMaxSelectableDates.kt new file mode 100644 index 00000000..2fdaccaa --- /dev/null +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/MinMaxSelectableDates.kt @@ -0,0 +1,13 @@ +package dev.appoutlet.foliary.core.ui.component.datepicker + +import androidx.compose.material3.SelectableDates +import kotlin.time.Instant + +class MinMaxSelectableDates( + private val minDateMillis: Long = Instant.DISTANT_PAST.toEpochMilliseconds(), + private val maxDateMillis: Long = Instant.DISTANT_FUTURE.toEpochMilliseconds(), +) : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + return utcTimeMillis in minDateMillis..maxDateMillis + } +} diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt index 49413217..aacf10c0 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreen.kt @@ -29,12 +29,15 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import dev.appoutlet.foliary.core.ui.component.button.FoliaryBackIconButton import dev.appoutlet.foliary.core.ui.component.button.FoliaryPrimaryButton +import dev.appoutlet.foliary.core.ui.component.datepicker.FoliaryDatePicker import dev.appoutlet.foliary.core.ui.component.modifier.widthInCompact import dev.appoutlet.foliary.core.ui.component.textfield.FoliaryTextField import dev.appoutlet.foliary.feature.main.getWindowDecorationPadding import foliary.foliary.generated.resources.Res import foliary.foliary.generated.resources.create_task_description_label import foliary.foliary.generated.resources.create_task_description_placeholder +import foliary.foliary.generated.resources.create_task_due_date_label +import foliary.foliary.generated.resources.create_task_due_date_placeholder import foliary.foliary.generated.resources.create_task_save import foliary.foliary.generated.resources.create_task_title import foliary.foliary.generated.resources.create_task_title_label @@ -63,6 +66,18 @@ fun CreateTaskScreen(viewData: CreateTaskViewData, onEvent: (CreateTaskEvent) -> Spacer(Modifier.size(16.dp)) TitleField(viewData.title, onEvent) DescriptionField(viewData.description, onEvent) + FoliaryDatePicker( + modifier = Modifier.widthInCompact() + .fillMaxWidth() + .padding(horizontal = 16.dp) + .testTag("CreateTaskScreen:DueDatePicker"), + label = stringResource(Res.string.create_task_due_date_label), + placeholder = stringResource(Res.string.create_task_due_date_placeholder), + selectedDate = viewData.dueDate?.selectedDateMillis, + selectedDateDisplayText = viewData.dueDate?.selectedDateDisplayText, + onDateSelected = { onEvent(CreateTaskEvent.DueDateChanged(it)) }, + minDateMillis = viewData.minDueDateMillis, + ) Spacer(Modifier.size(64.dp)) } diff --git a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt index aca629f4..8ae905cd 100644 --- a/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt +++ b/foliary/src/commonMain/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModel.kt @@ -7,6 +7,7 @@ import dev.appoutlet.foliary.core.provider.uuid.UuidProvider import dev.appoutlet.foliary.data.task.TaskRepository import dev.appoutlet.foliary.data.task.database.entity.Task import org.koin.core.annotation.KoinViewModel +import kotlin.time.Instant import kotlin.uuid.Uuid @KoinViewModel @@ -15,17 +16,35 @@ class CreateTaskViewModel( private val timeProvider: TimeProvider, private val uuidProvider: UuidProvider, ) : MviViewModel() { - override val container = container(CreateTaskViewData()) + override val container = container( + CreateTaskViewData( + minDueDateMillis = timeProvider.startOfToday().toEpochMilliseconds(), + ) + ) fun onEvent(event: CreateTaskEvent) { when (event) { is CreateTaskEvent.TitleChanged -> onTitleChange(event.title) is CreateTaskEvent.DescriptionChanged -> onDescriptionChange(event.description) + is CreateTaskEvent.DueDateChanged -> onDueDateChange(event.dueDateMillis) CreateTaskEvent.SaveClicked -> onSaveClick() CreateTaskEvent.BackClicked -> onBackClick() } } + private fun onDueDateChange(dueDateMillis: Long?) = intent { + reduce { + state.copy( + dueDate = dueDateMillis?.let { + CreateTaskViewData.DueDateViewData( + selectedDateMillis = it, + selectedDateDisplayText = timeProvider.displayText(it.toInstant()), + ) + } + ) + } + } + private fun onTitleChange(title: String) = intent { reduce { state.copy( @@ -47,7 +66,9 @@ class CreateTaskViewModel( title = state.title.trim(), description = state.description?.trim(), creationDate = timeProvider.now(), - dueDate = null, + dueDate = state.dueDate?.selectedDateMillis?.toInstant()?.let { + timeProvider.endOfDay(it) + }, completionDate = null, priority = null, url = null, @@ -62,18 +83,32 @@ class CreateTaskViewModel( private fun onBackClick() = intent { postSideEffect(CreateTaskAction.NavigateBack) } + + private fun Long.toInstant() = Instant.fromEpochMilliseconds(this) } data class CreateTaskViewData( val id: String? = null, val title: String = "", val description: String? = null, + val dueDate: DueDateViewData? = null, + val minDueDateMillis: Long, val saveButtonEnabled: Boolean = false, -) +) { + data class DueDateViewData( + val selectedDateMillis: Long, + val selectedDateDisplayText: String, + ) { + companion object + } + + companion object +} sealed interface CreateTaskEvent { data class TitleChanged(val title: String) : CreateTaskEvent data class DescriptionChanged(val description: String?) : CreateTaskEvent + data class DueDateChanged(val dueDateMillis: Long?) : CreateTaskEvent data object SaveClicked : CreateTaskEvent data object BackClicked : CreateTaskEvent } diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePickerTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePickerTest.kt new file mode 100644 index 00000000..b5300447 --- /dev/null +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/FoliaryDatePickerTest.kt @@ -0,0 +1,146 @@ +package dev.appoutlet.foliary.core.ui.component.datepicker + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runComposeUiTest +import foliary.foliary.generated.resources.Res +import foliary.foliary.generated.resources.create_task_due_date_clear_a11y +import foliary.foliary.generated.resources.create_task_due_date_placeholder +import foliary.foliary.generated.resources.general_confirm +import io.kotest.matchers.shouldBe +import org.jetbrains.compose.resources.getString +import kotlin.test.Test + +@OptIn(ExperimentalTestApi::class) +class FoliaryDatePickerTest { + + @Test + fun `should show placeholder when selectedDate is null`() = runComposeUiTest { + val placeholder = getString(Res.string.create_task_due_date_placeholder) + + setContent { + FoliaryDatePicker( + label = "Due date", + placeholder = placeholder, + selectedDate = null, + selectedDateDisplayText = null, + onDateSelected = {} + ) + } + + onNodeWithText(placeholder).assertIsDisplayed() + onNodeWithTag("FoliaryDatePicker:Value", useUnmergedTree = true).assertIsDisplayed() + onNodeWithTag("FoliaryDatePicker:ClearButton", useUnmergedTree = true).assertDoesNotExist() + } + + @Test + fun `should show selected date when selectedDate is not null`() = runComposeUiTest { + val placeholder = getString(Res.string.create_task_due_date_placeholder) + val selectedDateDisplayText = "2026-07-21" + + setContent { + FoliaryDatePicker( + label = "Due date", + placeholder = placeholder, + selectedDate = 1_752_960_000_000, + selectedDateDisplayText = selectedDateDisplayText, + onDateSelected = {} + ) + } + + onNodeWithText(selectedDateDisplayText).assertIsDisplayed() + onNodeWithTag("FoliaryDatePicker:Value", useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun `should show clear button only when date is selected`() = runComposeUiTest { + val placeholder = getString(Res.string.create_task_due_date_placeholder) + val clearDescription = getString(Res.string.create_task_due_date_clear_a11y) + + setContent { + FoliaryDatePicker( + label = "Due date", + placeholder = placeholder, + selectedDate = null, + selectedDateDisplayText = null, + onDateSelected = {} + ) + } + + onNodeWithContentDescription(clearDescription).assertDoesNotExist() + } + + @Test + fun `clear button should call onDateSelected with null`() = runComposeUiTest { + val placeholder = getString(Res.string.create_task_due_date_placeholder) + val clearDescription = getString(Res.string.create_task_due_date_clear_a11y) + var cleared = false + + setContent { + FoliaryDatePicker( + label = "Due date", + placeholder = placeholder, + selectedDate = 1_752_960_000_000, + selectedDateDisplayText = "2026-07-21", + onDateSelected = { cleared = it == null } + ) + } + + onNodeWithContentDescription(clearDescription) + .assertIsDisplayed() + .performClick() + + cleared shouldBe true + } + + @Test + fun `should open date picker bottom sheet when card is clicked`() = runComposeUiTest { + val placeholder = getString(Res.string.create_task_due_date_placeholder) + val confirmText = getString(Res.string.general_confirm) + + setContent { + FoliaryDatePicker( + label = "Due date", + placeholder = placeholder, + selectedDate = null, + selectedDateDisplayText = null, + onDateSelected = {} + ) + } + + onNodeWithTag("FoliaryDatePicker:Card", useUnmergedTree = true).performClick() + + waitForIdle() + + onNodeWithText(confirmText).assertExists() + } + + @Test + fun `should not open date picker bottom sheet when clear button is clicked`() = runComposeUiTest { + val placeholder = getString(Res.string.create_task_due_date_placeholder) + val clearDescription = getString(Res.string.create_task_due_date_clear_a11y) + val confirmText = getString(Res.string.general_confirm) + + setContent { + FoliaryDatePicker( + label = "Due date", + placeholder = placeholder, + selectedDate = 1_752_960_000_000, + selectedDateDisplayText = "2026-07-21", + onDateSelected = {} + ) + } + + onNodeWithContentDescription(clearDescription) + .assertIsDisplayed() + .performClick() + + waitForIdle() + + onNodeWithText(confirmText).assertDoesNotExist() + } +} diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/MinMaxSelectableDatesTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/MinMaxSelectableDatesTest.kt new file mode 100644 index 00000000..d0c1f97a --- /dev/null +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/core/ui/component/datepicker/MinMaxSelectableDatesTest.kt @@ -0,0 +1,51 @@ +package dev.appoutlet.foliary.core.ui.component.datepicker + +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.time.Instant + +class MinMaxSelectableDatesTest { + + @Test + fun `should allow any date when using default bounds`() = runTest { + val subject = MinMaxSelectableDates() + + subject.isSelectableDate(Instant.parse("2026-07-21T00:00:00Z").toEpochMilliseconds()) shouldBe true + } + + @Test + fun `should allow date equal to min bound`() = runTest { + val subject = MinMaxSelectableDates(minDateMillis = 100) + + subject.isSelectableDate(100) shouldBe true + } + + @Test + fun `should allow date equal to max bound`() = runTest { + val subject = MinMaxSelectableDates(maxDateMillis = 100) + + subject.isSelectableDate(100) shouldBe true + } + + @Test + fun `should allow date between min and max bounds`() = runTest { + val subject = MinMaxSelectableDates(minDateMillis = 100, maxDateMillis = 200) + + subject.isSelectableDate(150) shouldBe true + } + + @Test + fun `should reject date below min bound`() = runTest { + val subject = MinMaxSelectableDates(minDateMillis = 100) + + subject.isSelectableDate(99) shouldBe false + } + + @Test + fun `should reject date above max bound`() = runTest { + val subject = MinMaxSelectableDates(maxDateMillis = 100) + + subject.isSelectableDate(101) shouldBe false + } +} diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/time/DefaultTimeProviderTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/time/DefaultTimeProviderTest.kt index ac3dc940..280b6487 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/time/DefaultTimeProviderTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/data/time/DefaultTimeProviderTest.kt @@ -16,6 +16,14 @@ class DefaultTimeProviderTest { subject.now().toEpochMilliseconds() shouldBeAtLeast now.toEpochMilliseconds() } + @Test + fun `should return the start of today for the configured time zone`() { + val timezone = TimeZone.UTC + val today = Instant.parse("2026-07-22T02:10:02.999Z") + val subject = DefaultTimeProvider(clock = FixedClock(today)) + subject.startOfToday(timezone) shouldBe Instant.parse("2026-07-22T00:00:00Z") + } + @Test fun `should return the end of today for the configured time zone`() { val timezone = TimeZone.UTC @@ -24,6 +32,38 @@ class DefaultTimeProviderTest { subject.endOfToday(timezone) shouldBe Instant.parse("2026-07-22T23:59:59.999999999Z") } + @Test + fun `should return the end of day for the configured time zone`() { + val timezone = TimeZone.UTC + val instant = Instant.parse("2026-07-22T02:10:02.999Z") + val subject = DefaultTimeProvider() + subject.endOfDay(instant, timezone) shouldBe Instant.parse("2026-07-22T23:59:59.999999999Z") + } + + @Test + fun `should return the end of day respecting the time zone`() { + val instant = Instant.parse("2026-07-22T02:10:02.999Z") + val subject = DefaultTimeProvider() + subject.endOfDay(instant, TimeZone.UTC) shouldBe Instant.parse("2026-07-22T23:59:59.999999999Z") + subject.endOfDay(instant, TimeZone.of("UTC-10")) shouldBe Instant.parse("2026-07-22T09:59:59.999999999Z") + } + + @Test + fun `should format instant to display text for the configured time zone`() { + val timezone = TimeZone.UTC + val instant = Instant.parse("2026-07-22T02:10:02.999Z") + val subject = DefaultTimeProvider() + subject.displayText(instant, timezone) shouldBe "22 Jul 2026" + } + + @Test + fun `should format instant to display text respecting the time zone`() { + val instant = Instant.parse("2026-07-22T02:10:02.999Z") + val subject = DefaultTimeProvider() + subject.displayText(instant, TimeZone.UTC) shouldBe "22 Jul 2026" + subject.displayText(instant, TimeZone.of("UTC-10")) shouldBe "21 Jul 2026" + } + private class FixedClock(private val instant: Instant) : Clock { override fun now(): Instant = instant } diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt index f26de7ac..55d06508 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskScreenTest.kt @@ -1,5 +1,8 @@ package dev.appoutlet.foliary.feature.createtask +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsEnabled @@ -15,6 +18,9 @@ import foliary.foliary.generated.resources.Res import foliary.foliary.generated.resources.back_icon_button_a11y import foliary.foliary.generated.resources.create_task_description_label import foliary.foliary.generated.resources.create_task_description_placeholder +import foliary.foliary.generated.resources.create_task_due_date_clear_a11y +import foliary.foliary.generated.resources.create_task_due_date_label +import foliary.foliary.generated.resources.create_task_due_date_placeholder import foliary.foliary.generated.resources.create_task_save import foliary.foliary.generated.resources.create_task_title import foliary.foliary.generated.resources.create_task_title_label @@ -28,7 +34,15 @@ class CreateTaskScreenTest { @Test fun `should render create task form`() = runComposeUiTest { setContent { - CreateTaskScreen(viewData = CreateTaskViewData(), onEvent = {}) + CreateTaskScreen( + viewData = CreateTaskViewData.fixture( + title = "", + description = null, + dueDate = null, + saveButtonEnabled = false, + ), + onEvent = {} + ) } onNodeWithText(getString(Res.string.create_task_title)).assertIsDisplayed() @@ -36,6 +50,10 @@ class CreateTaskScreenTest { onNodeWithText(getString(Res.string.create_task_title_placeholder)).assertIsDisplayed() onNodeWithText(getString(Res.string.create_task_description_label)).assertIsDisplayed() onNodeWithText(getString(Res.string.create_task_description_placeholder)).assertIsDisplayed() + onNodeWithText(getString(Res.string.create_task_due_date_label)).assertIsDisplayed() + onNodeWithText(getString(Res.string.create_task_due_date_placeholder)).assertIsDisplayed() + onNodeWithContentDescription(getString(Res.string.create_task_due_date_clear_a11y)) + .assertDoesNotExist() onNodeWithText(getString(Res.string.create_task_save)).assertIsDisplayed().assertIsNotEnabled() } @@ -45,7 +63,7 @@ class CreateTaskScreenTest { setContent { CreateTaskScreen( - viewData = CreateTaskViewData(saveButtonEnabled = true), + viewData = CreateTaskViewData.fixture(), onEvent = events::add, ) } @@ -68,7 +86,15 @@ class CreateTaskScreenTest { val description = "Use the garden store nearby" setContent { - CreateTaskScreen(viewData = CreateTaskViewData(), onEvent = {}) + CreateTaskScreen( + viewData = CreateTaskViewData.fixture( + title = "", + description = null, + dueDate = null, + saveButtonEnabled = false, + ), + onEvent = {} + ) } onNodeWithTag("CreateTaskScreen:TitleField", useUnmergedTree = true) @@ -81,4 +107,37 @@ class CreateTaskScreenTest { onNodeWithTag("CreateTaskScreen:DescriptionField", useUnmergedTree = true) .assertTextEquals(description) } + + @Test + fun `should show selected due date and clear it`() = runComposeUiTest { + val dueDate = CreateTaskViewData.DueDateViewData.fixture() + val events = mutableListOf() + var viewData by mutableStateOf(CreateTaskViewData.fixture(dueDate = null)) + + setContent { + CreateTaskScreen(viewData = viewData, onEvent = events::add) + } + + onNodeWithText(getString(Res.string.create_task_due_date_label)).assertIsDisplayed() + onNodeWithText(getString(Res.string.create_task_due_date_placeholder)).assertIsDisplayed() + onNodeWithContentDescription(getString(Res.string.create_task_due_date_clear_a11y)) + .assertDoesNotExist() + + viewData = CreateTaskViewData.fixture(dueDate = dueDate) + waitForIdle() + + onNodeWithText(dueDate.selectedDateDisplayText).assertIsDisplayed() + onNodeWithContentDescription(getString(Res.string.create_task_due_date_clear_a11y)) + .assertIsDisplayed() + .performClick() + + events.contains(CreateTaskEvent.DueDateChanged(null)) shouldBe true + + viewData = CreateTaskViewData.fixture(dueDate = null) + waitForIdle() + + onNodeWithText(getString(Res.string.create_task_due_date_placeholder)).assertIsDisplayed() + onNodeWithContentDescription(getString(Res.string.create_task_due_date_clear_a11y)) + .assertDoesNotExist() + } } diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt new file mode 100644 index 00000000..0a9b3da0 --- /dev/null +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewDataFixture.kt @@ -0,0 +1,25 @@ +package dev.appoutlet.foliary.feature.createtask + +fun CreateTaskViewData.Companion.fixture( + id: String? = null, + title: String = "Task fixture", + description: String? = "Task description", + dueDate: CreateTaskViewData.DueDateViewData? = CreateTaskViewData.DueDateViewData.fixture(), + minDueDateMillis: Long = 1_752_960_000_000, + saveButtonEnabled: Boolean = true, +) = CreateTaskViewData( + id = id, + title = title, + description = description, + dueDate = dueDate, + minDueDateMillis = minDueDateMillis, + saveButtonEnabled = saveButtonEnabled, +) + +fun CreateTaskViewData.DueDateViewData.Companion.fixture( + selectedDateMillis: Long = 1_752_960_000_000, + selectedDateDisplayText: String = "21 Jul 2026", +) = CreateTaskViewData.DueDateViewData( + selectedDateMillis = selectedDateMillis, + selectedDateDisplayText = selectedDateDisplayText, +) diff --git a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt index a69d752c..c7a646a0 100644 --- a/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt +++ b/foliary/src/commonTest/kotlin/dev/appoutlet/foliary/feature/createtask/CreateTaskViewModelTest.kt @@ -9,11 +9,13 @@ import dev.mokkery.MockMode import dev.mokkery.answering.returns import dev.mokkery.every import dev.mokkery.everySuspend +import dev.mokkery.matcher.any import dev.mokkery.matcher.capture.Capture import dev.mokkery.matcher.capture.capture import dev.mokkery.matcher.capture.get import dev.mokkery.mock import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import kotlin.test.Test import kotlin.time.Instant import kotlin.uuid.Uuid @@ -23,12 +25,18 @@ class CreateTaskViewModelTest : private val mockTaskRepository = mock(mode = MockMode.autoUnit) private val mockTimeProvider = mock() private val mockUuidProvider = mock() - - override fun createViewModel() = CreateTaskViewModel( - taskRepository = mockTaskRepository, - timeProvider = mockTimeProvider, - uuidProvider = mockUuidProvider - ) + private val startOfToday = Instant.parse("2026-07-22T00:00:00Z") + private val expectedDisplayText = "22 Jul 2026" + + override fun createViewModel(): CreateTaskViewModel { + every(mockTimeProvider::startOfToday) returns startOfToday + every { mockTimeProvider.displayText(any()) } returns expectedDisplayText + return CreateTaskViewModel( + taskRepository = mockTaskRepository, + timeProvider = mockTimeProvider, + uuidProvider = mockUuidProvider + ) + } @Test fun `TitleChanged - blank title button disabled`() = test { @@ -65,6 +73,40 @@ class CreateTaskViewModelTest : expectSideEffect(CreateTaskAction.NavigateBack) } + @Test + fun `default state should have dueDate unset and minDueDate set to start of today`() = test { + currentState.dueDate shouldBe null + currentState.minDueDateMillis shouldBe startOfToday.toEpochMilliseconds() + } + + @Test + fun `DueDateChanged - should update dueDate`() = test { + val dueDateMillis = 1_752_996_000_000 + + viewModel.onEvent(CreateTaskEvent.DueDateChanged(dueDateMillis)) + + awaitState().dueDate.also { + it?.selectedDateMillis shouldBe dueDateMillis + it?.selectedDateDisplayText shouldNotBe null + } + } + + @Test + fun `DueDateChanged - null should clear dueDate`() = test { + val dueDateMillis = 1_752_996_000_000 + val expectedDueDate = CreateTaskViewData.DueDateViewData.fixture( + selectedDateMillis = dueDateMillis, + selectedDateDisplayText = expectedDisplayText, + ) + + viewModel.onEvent(CreateTaskEvent.DueDateChanged(dueDateMillis)) + expectState { copy(dueDate = expectedDueDate) } + + viewModel.onEvent(CreateTaskEvent.DueDateChanged(null)) + + awaitState().dueDate shouldBe null + } + @Test fun `SaveClicked - save task on task creation flow`() = test { val id = Uuid.random() @@ -77,7 +119,13 @@ class CreateTaskViewModelTest : every(mockTimeProvider::now) returns creationDate everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit - currentState shouldBe CreateTaskViewData() + currentState shouldBe CreateTaskViewData.fixture( + title = "", + description = null, + dueDate = null, + minDueDateMillis = startOfToday.toEpochMilliseconds(), + saveButtonEnabled = false, + ) viewModel.onEvent(CreateTaskEvent.TitleChanged(title)) expectState { copy(title = title, saveButtonEnabled = true) } @@ -95,9 +143,48 @@ class CreateTaskViewModelTest : task.title shouldBe title task.description shouldBe description task.creationDate shouldBe creationDate + task.dueDate shouldBe null } // Navigate back after successful saving awaitSideEffect() shouldBe CreateTaskAction.NavigateBack } + + @Test + fun `SaveClicked - should persist selected dueDate as end of day`() = test { + val id = Uuid.random() + val title = "Task title" + val dueDate = Instant.parse("2026-07-21T12:00:00Z") + val dueDateMillis = dueDate.toEpochMilliseconds() + val endOfDayDueDate = Instant.parse("2026-07-21T23:59:59.999999999Z") + val taskCapture = Capture.slot() + val creationDate = Instant.parse("2026-07-21T12:00:00Z") + + every(mockUuidProvider::random) returns id + every(mockTimeProvider::now) returns creationDate + every { mockTimeProvider.endOfDay(dueDate) } returns endOfDayDueDate + everySuspend { mockTaskRepository.save(capture(taskCapture)) } returns Unit + + viewModel.onEvent(CreateTaskEvent.TitleChanged(title)) + expectState { copy(title = title, saveButtonEnabled = true) } + + viewModel.onEvent(CreateTaskEvent.DueDateChanged(dueDateMillis)) + expectState { + copy( + title = title, + dueDate = CreateTaskViewData.DueDateViewData.fixture( + selectedDateMillis = dueDateMillis, + selectedDateDisplayText = expectedDisplayText, + ), + saveButtonEnabled = true, + ) + } + + viewModel.onEvent(CreateTaskEvent.SaveClicked) + awaitState().saveButtonEnabled shouldBe false + + taskCapture.get().dueDate shouldBe endOfDayDueDate + + awaitSideEffect() shouldBe CreateTaskAction.NavigateBack + } } diff --git a/i18n.lock b/i18n.lock index 4a98c724..da6b1e8c 100644 --- a/i18n.lock +++ b/i18n.lock @@ -43,6 +43,5 @@ checksums: create_task_save: f7a2929f33bc420195e59ac5a8bcd454 create_task_title_required: a8541a9d81ad1f2d007e52328d8b4074 task_item_overdue: 24caaa2b5d7a2447ab7664e3771cf98c - open_app: 98d48f4e2cbf39c43abbc6dd32eb4ff9 - quit: e4546c0aa9f1fa3e788cadec5e62f8ef - back_icon_button_a11y: b9c9db6a69a2e078c3be411b12c6097f + system_tray_open_app: 98d48f4e2cbf39c43abbc6dd32eb4ff9 + system_tray_quit: e4546c0aa9f1fa3e788cadec5e62f8ef