fix: single branded splash (invisible system icon), December title, first-open lag
- System splash now uses a transparent icon, so there is no system icon that shrinks into the in-app splash: just black, then the branded screen (icon + Calendarr name + copyright), held until data is loaded - Month/quarter title uses TextStyle.FULL (format style) with a hard fallback; FULL_STANDALONE had the same desugar bug as the LLLL pattern (December showed only the year) - First open: load the full +/- cacheMonths window behind the splash and only then reveal, so the app no longer enters mid-load and stutters Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,21 +12,30 @@ import java.time.format.TextStyle
|
||||
|
||||
private val zone: ZoneId = ZoneId.systemDefault()
|
||||
|
||||
/** Localized month name with a hard fallback (guards against empty/numeric results). */
|
||||
private fun monthName(date: LocalDate, style: TextStyle, loc: java.util.Locale): String {
|
||||
val name = date.month.getDisplayName(style, loc)
|
||||
val safe = if (name.isBlank() || name.all { it.isDigit() }) {
|
||||
date.month.getDisplayName(TextStyle.FULL, java.util.Locale.ENGLISH)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
return safe.replaceFirstChar { it.uppercase(loc) }
|
||||
}
|
||||
|
||||
fun titleForView(viewType: CalViewType, date: LocalDate, lang: String): String {
|
||||
val loc = L10n.locale(lang)
|
||||
return when (viewType) {
|
||||
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) }
|
||||
// Use the FORMAT month style (TextStyle.FULL). The standalone style
|
||||
// (FULL_STANDALONE / the "LLLL" pattern) drops the name for some
|
||||
// months under desugared java.time (e.g. December showed only "2026").
|
||||
val month = monthName(date, TextStyle.FULL, loc)
|
||||
"$month ${date.year}"
|
||||
}
|
||||
CalViewType.QUARTER -> {
|
||||
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}"
|
||||
"${monthName(date, TextStyle.SHORT, loc)} ${date.year} – ${monthName(endM, TextStyle.SHORT, loc)} ${endM.year}"
|
||||
}
|
||||
CalViewType.WEEK -> {
|
||||
val start = date
|
||||
|
||||
@@ -64,9 +64,24 @@ class CalendarViewModel @Inject constructor(
|
||||
private var allCachedEvents: List<CalEvent> = emptyList()
|
||||
|
||||
init {
|
||||
loadVisible()
|
||||
loadWritableCalendars()
|
||||
prefetchBackground()
|
||||
initialLoad()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the full ±cacheMonths window once, behind the splash, and only then
|
||||
* mark ready. Entering the app fully-loaded avoids the first-open jank of
|
||||
* loading a big batch while the user is already scrolling.
|
||||
*/
|
||||
private fun initialLoad() {
|
||||
val months = settingsStore.cacheMonths.toLong()
|
||||
val today = LocalDate.now().withDayOfMonth(1)
|
||||
val start = instant(today.minusMonths(months))
|
||||
val end = instant(today.plusMonths(months + 1))
|
||||
viewModelScope.launch {
|
||||
loadRange(start, end, background = false)
|
||||
markReady()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialState(): CalendarUiState {
|
||||
@@ -188,15 +203,6 @@ class CalendarViewModel @Inject constructor(
|
||||
_ready.value = true
|
||||
}
|
||||
|
||||
private fun prefetchBackground() {
|
||||
val months = settingsStore.cacheMonths
|
||||
val today = LocalDate.now().withDayOfMonth(1)
|
||||
val start = instant(today.minusMonths(months.toLong()))
|
||||
val end = instant(today.plusMonths((months + 1).toLong()))
|
||||
if (isCached(start, end)) return
|
||||
viewModelScope.launch { loadRange(start, end, background = true) }
|
||||
}
|
||||
|
||||
private fun isCached(start: Instant, end: Instant): Boolean {
|
||||
val cs = cachedStart ?: return false
|
||||
val ce = cachedEnd ?: return false
|
||||
@@ -231,8 +237,7 @@ class CalendarViewModel @Inject constructor(
|
||||
|
||||
fun syncWithServer() {
|
||||
invalidateCache()
|
||||
loadVisible(force = true)
|
||||
prefetchBackground()
|
||||
initialLoad()
|
||||
}
|
||||
|
||||
fun clearError() = _state.update { it.copy(error = null) }
|
||||
@@ -280,8 +285,7 @@ class CalendarViewModel @Inject constructor(
|
||||
|
||||
private fun afterMutation() {
|
||||
invalidateCache()
|
||||
loadVisible(force = true)
|
||||
prefetchBackground()
|
||||
initialLoad()
|
||||
}
|
||||
|
||||
fun saveEvent(
|
||||
|
||||
7
app/src/main/res/drawable/splash_transparent.xml
Normal file
7
app/src/main/res/drawable/splash_transparent.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Invisible system-splash icon: the system splash is just black, so the
|
||||
branded in-app splash (icon + name + copyright) is the only thing seen. -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
||||
@@ -4,8 +4,5 @@
|
||||
<item name="android:statusBarColor">@android:color/black</item>
|
||||
<item name="android:navigationBarColor">@android:color/black</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<!-- Android 12+ system splash screen -->
|
||||
<item name="android:windowSplashScreenBackground">@android:color/black</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
</style>
|
||||
|
||||
<!-- System splash (core-splashscreen); covers the window from frame 0. -->
|
||||
<!-- System splash (core-splashscreen): plain black with an invisible icon,
|
||||
so the only branded screen the user sees is the in-app splash. -->
|
||||
<style name="Theme.Calendarr.Starting" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@android:color/black</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_transparent</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Calendarr</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user