Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
fb85b00
Implement elastic top spacing for the first pinned row in the iOS tim…
OmarI-Kubra May 19, 2026
3fad667
Implement elastic header and row spacing for the iOS timeline screens.
OmarI-Kubra May 19, 2026
f605fed
Implement elastic top insets for the mode tabs in the iOS Calendar sc…
OmarI-Kubra May 19, 2026
33a47a9
Refine calendar day cell interactions in the Android Compose client.
OmarI-Kubra May 19, 2026
f2b017f
Refactor calendar view mode selection to use native segmented control…
OmarI-Kubra May 19, 2026
c356fce
Refactor calendar title and mode tab layout in the iOS SwiftUI client.
OmarI-Kubra May 19, 2026
93e4882
Redesign the calendar view mode tabs and refine the mini-calendar UI …
OmarI-Kubra May 19, 2026
83537b1
Refine the calendar UI and task timeline interactions across Android …
OmarI-Kubra May 19, 2026
d5c2946
Implement interactive pop gesture and refine `TodoListScreen` UI for …
OmarI-Kubra May 19, 2026
19f4ad1
Enable interactive swipe-back gestures in the iOS app root view.
OmarI-Kubra May 19, 2026
313e0da
Refine interactive pop gesture reliability and navigation controller …
OmarI-Kubra May 19, 2026
9307078
Implement a custom header for the task creation sheet in the iOS Swif…
OmarI-Kubra May 19, 2026
2da0f7a
Refactor and unify calendar card metrics for both iOS and Android cli…
OmarI-Kubra May 19, 2026
2a53aab
Refactor the task creation and list settings UI on iOS and enhance th…
OmarI-Kubra May 19, 2026
cac5704
Refine the calendar UI for both Android (Compose) and iOS (SwiftUI) t…
OmarI-Kubra May 19, 2026
7a2e6dc
Refine the calendar UI for both Android (Compose) and iOS (SwiftUI) t…
OmarI-Kubra May 19, 2026
1d94a36
Refine the UI for the "Create List" sheet and todo timeline on both i…
OmarI-Kubra May 19, 2026
75aea36
Revert "Refine the UI for the "Create List" sheet and todo timeline o…
OmarI-Kubra May 19, 2026
8567fe3
Refactor task and list creation sheets for Android (Compose) and iOS …
OmarI-Kubra May 19, 2026
b0ee1d9
Refine the UI for task and list creation sheets on both Android and iOS.
OmarI-Kubra May 19, 2026
ba2de8f
Refactor the task creation sheet in the iOS SwiftUI client to replace…
OmarI-Kubra May 19, 2026
6bad8f8
Refactor `CreateTaskSheet` internal selector naming.
OmarI-Kubra May 20, 2026
f5825c1
Implement list creation timestamps and synchronize list ordering acro…
OmarI-Kubra May 20, 2026
e5defd9
Implement custom settings UI and enhance timeline section animations …
OmarI-Kubra May 20, 2026
d3d5fec
Refactor timeline section expansion and collapse logic across Android…
OmarI-Kubra May 20, 2026
55c27aa
Refactor `TodoListScreen` to remove empty placeholders in the timeline.
OmarI-Kubra May 20, 2026
7a39065
Implement collapsing top bar navigation for Settings and App Version …
OmarI-Kubra May 20, 2026
b7b2126
Remove item restoration functionality from the Completed screen in th…
OmarI-Kubra May 20, 2026
a74b3b8
Refactor task due date string formatting to use positional arguments.
OmarI-Kubra May 20, 2026
c76f28a
Refine task creation and completed screens across Android and iOS cli…
OmarI-Kubra May 20, 2026
751c171
Enhance `SettingsScreen` header animations and scroll behavior for th…
OmarI-Kubra May 20, 2026
3ad54cc
Improve keyboard and focus management in `CreateTaskBottomSheet`.
OmarI-Kubra May 20, 2026
88d51b3
Refactor the settings screen UI implementation from a `ScrollView` to…
OmarI-Kubra May 20, 2026
f19aa7d
Refactor `SettingsScreen` and the version release view in the iOS Swi…
OmarI-Kubra May 20, 2026
1dabed7
Refactor `SettingsScreen` and the version release view in the iOS Swi…
OmarI-Kubra May 20, 2026
5ca270e
Refine top bar action button styling and apply circular chrome to key…
OmarI-Kubra May 20, 2026
16b59a4
Update the Home screen search UI to support full-width expansion on i…
OmarI-Kubra May 20, 2026
927f559
Update the Home screen search UI to support full-width expansion on i…
OmarI-Kubra May 20, 2026
40add19
Adjust search input focus delay for iOS and Android.
OmarI-Kubra May 20, 2026
afe1eef
Refactor the home screen search bar for the iOS and Android clients.
OmarI-Kubra May 20, 2026
db83dec
Refine the presentation detent and height management logic for the Cr…
OmarI-Kubra May 20, 2026
fe58709
Adjust the maximum height for the "Create List" sheet in the iOS Swif…
OmarI-Kubra May 20, 2026
a054ecf
Implement dynamic height adjustment for the task creation sheet in th…
OmarI-Kubra May 20, 2026
b348458
Refine layout and background styling for the "Create List" sheet in t…
OmarI-Kubra May 20, 2026
b94d8b5
Refine layout and background styling for the "Create List" sheet in t…
OmarI-Kubra May 20, 2026
69612a5
Refine the checkmark color on the iOS Completed screen.
OmarI-Kubra May 20, 2026
a943931
Implement standard bottom sheet sizing for iOS 18+ in the SwiftUI cli…
OmarI-Kubra May 20, 2026
eabfdb8
Revert "Implement standard bottom sheet sizing for iOS 18+ in the Swi…
OmarI-Kubra May 20, 2026
b32d93c
Improve dynamic height handling and animations for "Create Task" and …
OmarI-Kubra May 20, 2026
f493e79
Merge remote-tracking branch 'origin/develop' into develop
ohmzi May 20, 2026
b9069fe
feat: implement custom month grid calendar and update layout
ohmzi May 21, 2026
23eeaea
feat(android,ios): improve offline mode persistence and sync reliability
ohmzi May 21, 2026
c269b7d
Polish pull to refresh animation
ohmzi May 21, 2026
75987aa
Polish mobile task empty states
ohmzi May 21, 2026
3a82722
Match iOS splash to Android
ohmzi May 21, 2026
2601eee
Add iOS dark mode theme support
ohmzi May 21, 2026
ab3e7c1
feat: implement task uncompletion and unify segmented control UI
ohmzi May 21, 2026
7275937
Add `TdaySegmentedSlider` component to Android Compose
ohmzi May 21, 2026
e003b37
Lighten iOS dark bottom sheets
ohmzi May 21, 2026
e48a622
Improve background consistency in `TodoListScreen`
ohmzi May 21, 2026
522f178
Polish home screen tile layout and hit testing
ohmzi May 21, 2026
a6fea3b
Standardize the brand color palette across Android and iOS by introdu…
OmarI-Kubra May 21, 2026
724c989
Polish mobile refresh and offline recovery
ohmzi May 21, 2026
9745177
Add iOS home search parity
ohmzi May 21, 2026
31bfd91
Fix iOS search results overlay
ohmzi May 21, 2026
676dc0a
Improve search-to-task navigation and scrolling behavior on Android a…
OmarI-Kubra May 21, 2026
48e0ad4
Refine search navigation and scroll-to-target animations for task sea…
OmarI-Kubra May 21, 2026
90757c2
Implement dynamic height calculation for the search results dropdown …
OmarI-Kubra May 21, 2026
0af846a
Refine pull-to-refresh UI and logic, and optimize list navigation on …
OmarI-Kubra May 21, 2026
5444c16
Implement automatic server reconnection on app foreground and enhance…
OmarI-Kubra May 21, 2026
6029221
Refine the pull-to-refresh indicator UI and animations in the iOS Swi…
OmarI-Kubra May 21, 2026
c108c4d
Refine pull-to-refresh mechanics, scroll behaviors, and UI dimensions…
OmarI-Kubra May 21, 2026
30dfa25
Implement a visual watermark and refined empty state messages across …
OmarI-Kubra May 21, 2026
04d5d7f
Implement a unified empty state UI for task lists using a combined wa…
OmarI-Kubra May 21, 2026
6976d31
Introduce a reusable watermark and background message component for e…
OmarI-Kubra May 21, 2026
2bb1306
Implement collapsible title snapping and improve list section orderin…
OmarI-Kubra May 22, 2026
845a1cf
Standardize timeline UI behavior, refine swipe action styling, and in…
OmarI-Kubra May 22, 2026
beed94d
Standardize timeline UI behavior, refine swipe action styling, and in…
OmarI-Kubra May 22, 2026
3b4df55
Implement an elastic, collapsing top bar for the Calendar screen in t…
OmarI-Kubra May 22, 2026
3032c2f
Polish mobile refresh and offline notices
ohmzi May 22, 2026
efa420b
Tighten iOS title spacing
ohmzi May 22, 2026
4984143
Implement an elastic, collapsing top bar for the Calendar screen in t…
ohmzi May 22, 2026
1f5f2dd
Implement a minimum display duration for the startup splash screen on…
ohmzi May 22, 2026
0c783bd
Set splash screen duration to 1.5 seconds
ohmzi May 22, 2026
208a3db
Implement connectivity issue classification for server unavailable st…
ohmzi May 22, 2026
547c6db
Implement manual hold-to-pause logic for the startup splash screen on…
ohmzi May 22, 2026
4b2724e
Implement a branded launch screen and improve the initial app loading…
ohmzi May 22, 2026
9dfbbc5
Update the iOS launch screen with a new logo and custom typography.
ohmzi May 22, 2026
6e67c1e
Improve error handling for network connectivity and update visual ass…
ohmzi May 22, 2026
e064629
Improve error handling for network connectivity and update visual ass…
ohmzi May 22, 2026
67e41c4
Update pull-to-refresh styling and behavior across Android and iOS, a…
ohmzi May 22, 2026
ae1b94b
Implement a native splash screen and optimize app startup on Android.
ohmzi May 22, 2026
34ac1e3
Fix stale iOS server probe errors
ohmzi May 22, 2026
3c5046d
Implement Android Credential Manager support for seamless login and r…
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