diff --git a/app/src/main/java/com/scarriffle/calendarr/MainActivity.kt b/app/src/main/java/com/scarriffle/calendarr/MainActivity.kt index 67cdd5b..8af7e9e 100644 --- a/app/src/main/java/com/scarriffle/calendarr/MainActivity.kt +++ b/app/src/main/java/com/scarriffle/calendarr/MainActivity.kt @@ -4,35 +4,16 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.lifecycle.lifecycleScope -import com.scarriffle.calendarr.data.CredentialStore -import com.scarriffle.calendarr.data.StartupState import com.scarriffle.calendarr.ui.CalendarrRoot import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { - - @Inject lateinit var startupState: StartupState - @Inject lateinit var credentialStore: CredentialStore - override fun onCreate(savedInstanceState: Bundle?) { - // Single, uniform splash: keep the system splash on screen until the - // first events have loaded (or a timeout), so there is no two-stage - // splash and no entering a half-loaded, janky app. - val splash = installSplashScreen() - splash.setKeepOnScreenCondition { !startupState.ready.value } - + // Covers the window from the first frame (incl. warm-start), then hands + // off to the in-app branded splash which stays until data is loaded. + installSplashScreen() super.onCreate(savedInstanceState) - - // Nothing to load before login → reveal immediately. - if (!credentialStore.isLoggedIn) startupState.markReady() - // Safety timeout so the splash can never hang. - lifecycleScope.launch { delay(6000); startupState.markReady() } - setContent { CalendarrRoot() } diff --git a/app/src/main/java/com/scarriffle/calendarr/data/StartupState.kt b/app/src/main/java/com/scarriffle/calendarr/data/StartupState.kt deleted file mode 100644 index 8d9b4e4..0000000 --- a/app/src/main/java/com/scarriffle/calendarr/data/StartupState.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.scarriffle.calendarr.data - -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Shared startup readiness flag used to keep the system splash screen on screen - * until the first events have loaded (so the app never appears mid-load). - */ -@Singleton -class StartupState @Inject constructor() { - val ready = MutableStateFlow(false) - fun markReady() { ready.value = true } -} diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt b/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt index 3ef403e..9a53551 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/CalendarrRoot.kt @@ -5,14 +5,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import com.scarriffle.calendarr.ui.auth.LoginScreen import com.scarriffle.calendarr.ui.auth.ServerSetupScreen import com.scarriffle.calendarr.ui.calendar.CalendarScreen +import com.scarriffle.calendarr.ui.calendar.CalendarViewModel import com.scarriffle.calendarr.ui.theme.CalendarrTheme +import kotlinx.coroutines.delay @Composable fun CalendarrRoot(vm: MainViewModel = hiltViewModel()) { @@ -24,9 +30,23 @@ fun CalendarrRoot(vm: MainViewModel = hiltViewModel()) { LocalLang provides L10n.resolved(settings.language), LocalAppSettings provides settings, ) { - // The system splash (installSplashScreen) covers startup until the - // first events are loaded, so there's no in-app splash here. Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + // Obtain the calendar VM early when logged in so events load + // *behind* the splash; the branded splash stays until ready. + val calendarVm: CalendarViewModel? = + if (route == AppRoute.MAIN) hiltViewModel() else null + val dataReady = calendarVm?.ready?.collectAsState()?.value ?: true + + var minElapsed by remember { mutableStateOf(false) } + var timedOut by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { delay(500); minElapsed = true } + LaunchedEffect(Unit) { delay(6000); timedOut = true } + + if (!minElapsed || (!dataReady && !timedOut)) { + SplashScreen() + return@Surface + } + when (route) { AppRoute.SETUP -> ServerSetupScreen(onConfigured = vm::onServerConfigured) AppRoute.LOGIN -> LoginScreen( @@ -35,6 +55,7 @@ fun CalendarrRoot(vm: MainViewModel = hiltViewModel()) { onBack = vm::switchServer, ) AppRoute.MAIN -> CalendarScreen( + vm = calendarVm!!, onLogout = vm::logout, onSwitchServer = vm::switchServer, onSettingsChanged = vm::applyLocalSettings, diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/SplashScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/SplashScreen.kt new file mode 100644 index 0000000..d1615c1 --- /dev/null +++ b/app/src/main/java/com/scarriffle/calendarr/ui/SplashScreen.kt @@ -0,0 +1,57 @@ +package com.scarriffle.calendarr.ui + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.scarriffle.calendarr.R + +/** + * Branded startup screen shown right after the system splash hands off (same + * black background + same centred logo, so the logo doesn't jump); the app name + * and copyright fade in. Stays until the first events have loaded. + */ +@Composable +fun SplashScreen() { + // Fade the texts in so the handoff from the (text-less) system splash is smooth. + val textAlpha by animateFloatAsState(targetValue = 1f, animationSpec = tween(350), label = "splashText") + + Box(Modifier.fillMaxSize().background(Color.Black)) { + Text( + "Calendarr", + modifier = Modifier.align(Alignment.TopCenter).padding(top = 110.dp).alpha(textAlpha), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold, + color = Color.White, + ) + // Match the system splash icon size (~150dp) so it stays put across the handoff. + Image( + painter = painterResource(R.drawable.ic_splash_logo), + contentDescription = null, + modifier = Modifier.align(Alignment.Center).size(150.dp).clip(RoundedCornerShape(34.dp)), + ) + Text( + "© Scarriffle", + modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 40.dp).alpha(textAlpha), + style = MaterialTheme.typography.bodySmall, + color = Color.White.copy(alpha = 0.6f), + ) + } +} diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFormatting.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFormatting.kt index 0429f36..bbdd129 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFormatting.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarFormatting.kt @@ -15,12 +15,18 @@ private val zone: ZoneId = ZoneId.systemDefault() fun titleForView(viewType: CalViewType, date: LocalDate, lang: String): String { val loc = L10n.locale(lang) return when (viewType) { - CalViewType.MONTH -> - DateTimeFormatter.ofPattern("LLLL yyyy", loc).format(date) + CalViewType.MONTH -> { + // Use Month.getDisplayName (robust) instead of the "LLLL" pattern, + // which could drop the month name under desugared java.time. + val month = date.month.getDisplayName(TextStyle.FULL_STANDALONE, loc) .replaceFirstChar { it.uppercase(loc) } + "$month ${date.year}" + } CalViewType.QUARTER -> { - val fmt = DateTimeFormatter.ofPattern("LLL yyyy", loc) - "${fmt.format(date)} – ${fmt.format(date.plusMonths(2))}" + val endM = date.plusMonths(2) + val m1 = date.month.getDisplayName(TextStyle.SHORT_STANDALONE, loc).replaceFirstChar { it.uppercase(loc) } + val m2 = endM.month.getDisplayName(TextStyle.SHORT_STANDALONE, loc).replaceFirstChar { it.uppercase(loc) } + "$m1 ${date.year} – $m2 ${endM.year}" } CalViewType.WEEK -> { val start = date diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt index 9cc8767..7c62517 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.scarriffle.calendarr.data.CalendarRepository import com.scarriffle.calendarr.data.SettingsStore -import com.scarriffle.calendarr.data.StartupState import com.scarriffle.calendarr.domain.model.CalEvent import com.scarriffle.calendarr.domain.model.CalViewType import com.scarriffle.calendarr.domain.model.WritableCalendar @@ -45,7 +44,6 @@ data class CalendarUiState( class CalendarViewModel @Inject constructor( private val repository: CalendarRepository, private val settingsStore: SettingsStore, - private val startupState: StartupState, ) : ViewModel() { private val zone: ZoneId = ZoneId.systemDefault() @@ -188,7 +186,6 @@ class CalendarViewModel @Inject constructor( private fun markReady() { _ready.value = true - startupState.markReady() } private fun prefetchBackground() { diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/MonthView.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/MonthView.kt index c4ecb00..bb6642c 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/MonthView.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/MonthView.kt @@ -55,8 +55,8 @@ import java.time.temporal.IsoFields import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -private const val MONTHS_BACK = 18L -private const val MONTHS_AHEAD = 18L +private const val MONTHS_BACK = 120L // 10 years back +private const val MONTHS_AHEAD = 600L // 50 years ahead private const val MAX_LANES = 4 private val DAY_NUM_H = 22.dp private val LANE_H = 15.dp