Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8471c3a
Merge remote-tracking branch 'origin/develop' into develop
ohmzi May 20, 2026
0727a36
feat: implement custom month grid calendar and update layout
ohmzi May 21, 2026
35528f6
feat(android,ios): improve offline mode persistence and sync reliability
ohmzi May 21, 2026
1aada63
Polish pull to refresh animation
ohmzi May 21, 2026
4f5ac67
Polish mobile task empty states
ohmzi May 21, 2026
aaa5453
Match iOS splash to Android
ohmzi May 21, 2026
13312e4
Add iOS dark mode theme support
ohmzi May 21, 2026
7101b29
feat: implement task uncompletion and unify segmented control UI
ohmzi May 21, 2026
ae1af7c
Add `TdaySegmentedSlider` component to Android Compose
ohmzi May 21, 2026
597f83c
Lighten iOS dark bottom sheets
ohmzi May 21, 2026
64ecc79
Improve background consistency in `TodoListScreen`
ohmzi May 21, 2026
2e083d3
Polish home screen tile layout and hit testing
ohmzi May 21, 2026
3520a95
Standardize the brand color palette across Android and iOS by introdu…
ohmzi May 21, 2026
b2054fe
Polish mobile refresh and offline recovery
ohmzi May 21, 2026
e1fdb3b
Add iOS home search parity
ohmzi May 21, 2026
da53478
Fix iOS search results overlay
ohmzi May 21, 2026
1639414
Improve search-to-task navigation and scrolling behavior on Android a…
ohmzi May 21, 2026
cbdcbe2
Refine search navigation and scroll-to-target animations for task sea…
ohmzi May 21, 2026
31a40fa
Implement dynamic height calculation for the search results dropdown …
ohmzi May 21, 2026
d6a1e7e
Refine pull-to-refresh UI and logic, and optimize list navigation on …
ohmzi May 21, 2026
399bac8
Implement automatic server reconnection on app foreground and enhance…
ohmzi May 21, 2026
8ef2250
Refine the pull-to-refresh indicator UI and animations in the iOS Swi…
ohmzi May 21, 2026
419b887
Refine pull-to-refresh mechanics, scroll behaviors, and UI dimensions…
ohmzi May 21, 2026
793fe45
Implement a visual watermark and refined empty state messages across …
ohmzi May 21, 2026
8d2ebf0
Implement a unified empty state UI for task lists using a combined wa…
ohmzi May 21, 2026
e701013
Introduce a reusable watermark and background message component for e…
ohmzi May 21, 2026
89e9c2d
Implement collapsible title snapping and improve list section orderin…
ohmzi May 22, 2026
5a442e8
Standardize timeline UI behavior, refine swipe action styling, and in…
ohmzi May 22, 2026
6bb718e
Standardize timeline UI behavior, refine swipe action styling, and in…
ohmzi May 22, 2026
86fe8bd
Implement an elastic, collapsing top bar for the Calendar screen in t…
ohmzi May 22, 2026
7c6d988
Polish mobile refresh and offline notices
ohmzi May 22, 2026
9c54fc5
Tighten iOS title spacing
ohmzi May 22, 2026
480830c
Implement an elastic, collapsing top bar for the Calendar screen in t…
ohmzi May 22, 2026
6a61269
Implement a minimum display duration for the startup splash screen on…
ohmzi May 22, 2026
5ee308f
Set splash screen duration to 1.5 seconds
ohmzi May 22, 2026
4d87a29
Implement connectivity issue classification for server unavailable st…
ohmzi May 22, 2026
94d387c
Implement manual hold-to-pause logic for the startup splash screen on…
ohmzi May 22, 2026
d9c7401
Implement a branded launch screen and improve the initial app loading…
ohmzi May 22, 2026
8474a07
Update the iOS launch screen with a new logo and custom typography.
ohmzi May 22, 2026
a8ae382
Improve error handling for network connectivity and update visual ass…
ohmzi May 22, 2026
331829c
Improve error handling for network connectivity and update visual ass…
ohmzi May 22, 2026
1309aa5
Update pull-to-refresh styling and behavior across Android and iOS, a…
ohmzi May 22, 2026
8596850
Implement a native splash screen and optimize app startup on Android.
ohmzi May 22, 2026
faf3661
Fix stale iOS server probe errors
ohmzi May 22, 2026
f18309d
Implement Android Credential Manager support for seamless login and r…
ohmzi May 22, 2026
0b536a9
Merge branch 'master' into develop
ohmzi May 22, 2026
a7fee0f
Refine "Today" timeline loading and animation transitions
ohmzi May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android-compose/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ dependencies {
implementation("androidx.compose.material:material-icons-extended:1.7.6")
implementation("androidx.navigation:navigation-compose:2.8.5")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.credentials:credentials:1.6.0")
implementation("androidx.credentials:credentials-play-services-auth:1.6.0")

implementation("com.google.dagger:hilt-android:2.57.2")
ksp("com.google.dagger:hilt-compiler:2.57.2")
Expand Down
5 changes: 4 additions & 1 deletion android-compose/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@
android:usesCleartextTraffic="${usesCleartextTraffic}">

<meta-data android:name="io.sentry.auto-init" android:value="false" />
<meta-data
android:name="asset_statements"
android:resource="@string/asset_statements" />
<activity
android:name=".MainActivity"
android:configChanges="keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.Tday">
android:theme="@style/Theme.Tday.Starting">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.ohmz.tday.compose

import android.Manifest
import android.content.Intent
import android.app.NotificationManager
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
Expand All @@ -26,13 +26,18 @@ class MainActivity : ComponentActivity() {
val deepLinkIntent = _deepLinkIntent.asStateFlow()

override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.Theme_Tday)
super.onCreate(savedInstanceState)
enableEdgeToEdge()
dismissUpdateReadyNotification()
requestNotificationPermissionIfNeeded()
_deepLinkIntent.value = intent
setContent {
TdayApp()
TdayApp(
onFirstFrameDrawn = {
(application as? TdayApplication)?.runDeferredStartup()
dismissUpdateReadyNotification()
requestNotificationPermissionIfNeeded()
},
)
}
}

Expand Down
155 changes: 143 additions & 12 deletions android-compose/app/src/main/java/com/ohmz/tday/compose/TdayApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
Expand All @@ -42,13 +44,16 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
Expand Down Expand Up @@ -96,12 +101,34 @@ private const val NAV_EXIT_DURATION_MS = 320
private const val NAV_FADE_IN_DURATION_MS = 360
private const val NAV_FADE_OUT_DURATION_MS = 240
private const val NAV_SLIDE_FRACTION = 0.18f
private const val PENDING_SEARCH_HIGHLIGHT_TODO_ID = "pendingSearchHighlightTodoId"
private const val SETTINGS_ENTER_DURATION_MS = 380
private const val SETTINGS_EXIT_DURATION_MS = 260
private const val SETTINGS_VERTICAL_FRACTION = 0.22f

@Composable
fun TdayApp() {
fun TdayApp(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`TdayApp` has a cyclomatic complexity of 16 with "High" risk


Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A function with high cyclomatic
complexity can be hard to understand and maintain. A higher cyclomatic
complexity indicates that the function has more decision points and is more complex.

onFirstFrameDrawn: () -> Unit = {},
) {
val startupTagline = rememberSaveable { splashTaglines.random() }
var hasDrawnStartupFrame by remember { mutableStateOf(false) }
val currentOnFirstFrameDrawn by rememberUpdatedState(onFirstFrameDrawn)

if (!hasDrawnStartupFrame) {
TdayTheme {
SplashScreen(
tagline = startupTagline,
onHoldChanged = {},
)
}
LaunchedEffect(Unit) {
withFrameNanos { }
hasDrawnStartupFrame = true
currentOnFirstFrameDrawn()
}
return
}

val navController = rememberNavController()

DisposableEffect(navController) {
Expand All @@ -122,6 +149,7 @@ fun TdayApp() {
val taskDeletedToastMessage = stringResource(R.string.task_deleted_toast)
var activeToast by remember { mutableStateOf<TdayToastData?>(null) }
var hasShownLaunchUpdateToast by rememberSaveable { mutableStateOf(false) }
var isStartupSplashHeld by remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current

Expand All @@ -138,6 +166,9 @@ fun TdayApp() {
appViewModel = appViewModel,
snackbarHostState = snackbarHostState,
)
OnAppForegroundResume {
appViewModel.reconnectAfterForeground()
}

fun showTaskDeletedToast() {
showSystemToast(context, taskDeletedToastMessage)
Expand All @@ -147,6 +178,7 @@ fun TdayApp() {
appUiState = appUiState,
currentRoute = currentRoute,
navController = navController,
isStartupSplashHeld = isStartupSplashHeld,
)

HandleLaunchUpdateToast(
Expand Down Expand Up @@ -235,23 +267,23 @@ fun TdayApp() {
enterTransition = { fadeIn(tween(300)) },
exitTransition = { fadeOut(tween(300)) },
) {
SplashScreen()
SplashScreen(onHoldChanged = { isStartupSplashHeld = it })
}

composable(
route = AppRoute.ServerSetup.route,
enterTransition = { fadeIn(tween(300)) },
exitTransition = { fadeOut(tween(300)) },
) {
SplashScreen()
SplashScreen(onHoldChanged = { isStartupSplashHeld = it })
}

composable(
route = AppRoute.Login.route,
enterTransition = { fadeIn(tween(300)) },
exitTransition = { fadeOut(tween(300)) },
) {
SplashScreen()
SplashScreen(onHoldChanged = { isStartupSplashHeld = it })
}

composable(
Expand Down Expand Up @@ -293,7 +325,10 @@ fun TdayApp() {
onOpenCalendar = { navController.navigate(AppRoute.Calendar.route) },
onOpenSettings = { navController.navigate(AppRoute.Settings.route) },
onOpenTaskFromSearch = { todoId ->
navController.navigate(AppRoute.AllTodos.create(highlightTodoId = todoId))
navController.currentBackStackEntry
?.savedStateHandle
?.set(PENDING_SEARCH_HIGHLIGHT_TODO_ID, todoId)
navController.navigate(AppRoute.AllTodos.create())
},
onOpenList = { id, name ->
navController.navigate(AppRoute.ListTodos.create(id, name))
Expand Down Expand Up @@ -367,8 +402,13 @@ fun TdayApp() {
},
)
},
onLogin = { email, password ->
authViewModel.login(email, password) {
onLogin = { email, password, source ->
authViewModel.login(
email = email,
password = password,
credentialContext = context,
source = source,
) {
appViewModel.refreshSession()
}
},
Expand All @@ -378,11 +418,13 @@ fun TdayApp() {
lastName = "",
email = email,
password = password,
credentialContext = context,
) {
onSuccess()
appViewModel.refreshSession()
}
},
onRequestSavedCredential = authViewModel::requestSavedCredential,
onClearAuthStatus = {
authViewModel.clearStatus()
appViewModel.clearPendingApprovalNotice()
Expand Down Expand Up @@ -453,9 +495,15 @@ fun TdayApp() {
navDeepLink { uriPattern = "tday://todos/all?highlightTodoId={highlightTodoId}" },
),
) { entry ->
val highlightTodoId = Uri.decode(
val pendingSearchHighlightTodoId = remember(entry) {
navController.previousBackStackEntry
?.savedStateHandle
?.remove<String>(PENDING_SEARCH_HIGHLIGHT_TODO_ID)
}
val argumentHighlightTodoId = Uri.decode(
entry.arguments?.getString("highlightTodoId").orEmpty(),
).ifBlank { null }
val highlightTodoId = pendingSearchHighlightTodoId ?: argumentHighlightTodoId
TodosRoute(
mode = TodoListMode.ALL,
highlightTodoId = highlightTodoId,
Expand Down Expand Up @@ -507,6 +555,7 @@ fun TdayApp() {
uiState = uiState,
onBack = { navController.popBackStack() },
onRefresh = viewModel::refresh,
onUncomplete = viewModel::uncomplete,
onDelete = { item ->
viewModel.delete(item) {
showTaskDeletedToast()
Expand Down Expand Up @@ -603,6 +652,7 @@ fun TdayApp() {
OfflineBanner(
visible = appUiState.isOffline && appUiState.authenticated,
pendingMutationCount = appUiState.pendingMutationCount,
noticeKey = appUiState.offlineNoticeId,
modifier = Modifier.align(Alignment.TopCenter),
)

Expand Down Expand Up @@ -653,13 +703,16 @@ private fun HandleStartupNavigation(
appUiState: AppUiState,
currentRoute: String?,
navController: NavHostController,
isStartupSplashHeld: Boolean,
) {
LaunchedEffect(
appUiState.loading,
appUiState.authenticated,
currentRoute,
isStartupSplashHeld,
) {
if (appUiState.loading) return@LaunchedEffect
if (isStartupSplashHeld) return@LaunchedEffect

if (appUiState.authenticated) {
val unauthenticatedRoutes = setOf(
Expand Down Expand Up @@ -807,6 +860,37 @@ private fun OnRouteResume(
}
}

@Composable
private fun OnAppForegroundResume(
action: () -> Unit,
) {
val lifecycleOwner = LocalLifecycleOwner.current
val currentAction by rememberUpdatedState(action)
DisposableEffect(lifecycleOwner) {
var hasPaused = false
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_PAUSE, Lifecycle.Event.ON_STOP -> {
hasPaused = true
}

Lifecycle.Event.ON_RESUME -> {
if (hasPaused) {
hasPaused = false
currentAction()
}
}

else -> Unit
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}

private fun navigationSlideOffset(fullDistance: Int): Int =
(fullDistance * NAV_SLIDE_FRACTION).roundToInt()

Expand Down Expand Up @@ -870,19 +954,64 @@ private val splashTaglines = listOf(
"Organizing your life, no landlord required",
"Zero trust\u2026 except your own server",
"Syncing your tasks, judging your priorities",
"Today called. It wants a plan.",
"Making later file a formal request",
"Turning chaos into checkboxes",
"Your tasks are lining up nicely",
"A private server with opinions about your priorities",
"For when your brain opens too many tabs",
"Scheduling the chaos before it schedules you",
"Your lists have entered their productive era",
"A tiny operations desk for future you",
"Because vibes are not a task strategy",
"Private tasks. Better mornings.",
"Making your backlog feel seen, then sorted",
"Where scattered thoughts get assigned seating",
"Your priorities just got a home address",
"Sync first, panic later",
"Calendar drama, now with containment",
"Deadlines hate this one self-hosted trick",
"Helping your day stop freelancing",
"Your reminders came prepared",
"Turning I should into scheduled",
)

@Composable
private fun SplashScreen() {
val tagline = remember { splashTaglines.random() }
private fun SplashScreen(
onHoldChanged: (Boolean) -> Unit,
tagline: String? = null,
) {
val resolvedTagline = tagline ?: remember { splashTaglines.random() }

DisposableEffect(onHoldChanged) {
onDispose { onHoldChanged(false) }
}

Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(onHoldChanged) {
detectTapGestures(
onPress = {
onHoldChanged(true)
try {
awaitRelease()
} finally {
onHoldChanged(false)
}
},
)
}
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Image(
painter = painterResource(id = R.drawable.splash_icon),
contentDescription = "T'Day",
Expand All @@ -896,9 +1025,11 @@ private fun SplashScreen() {
color = MaterialTheme.colorScheme.onBackground,
)
Text(
text = tagline,
text = resolvedTagline,
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
Expand Down
Loading
Loading