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()
|
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 {
|
fun titleForView(viewType: CalViewType, date: LocalDate, lang: String): String {
|
||||||
val loc = L10n.locale(lang)
|
val loc = L10n.locale(lang)
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
CalViewType.MONTH -> {
|
CalViewType.MONTH -> {
|
||||||
// Use Month.getDisplayName (robust) instead of the "LLLL" pattern,
|
// Use the FORMAT month style (TextStyle.FULL). The standalone style
|
||||||
// which could drop the month name under desugared java.time.
|
// (FULL_STANDALONE / the "LLLL" pattern) drops the name for some
|
||||||
val month = date.month.getDisplayName(TextStyle.FULL_STANDALONE, loc)
|
// months under desugared java.time (e.g. December showed only "2026").
|
||||||
.replaceFirstChar { it.uppercase(loc) }
|
val month = monthName(date, TextStyle.FULL, loc)
|
||||||
"$month ${date.year}"
|
"$month ${date.year}"
|
||||||
}
|
}
|
||||||
CalViewType.QUARTER -> {
|
CalViewType.QUARTER -> {
|
||||||
val endM = date.plusMonths(2)
|
val endM = date.plusMonths(2)
|
||||||
val m1 = date.month.getDisplayName(TextStyle.SHORT_STANDALONE, loc).replaceFirstChar { it.uppercase(loc) }
|
"${monthName(date, TextStyle.SHORT, loc)} ${date.year} – ${monthName(endM, TextStyle.SHORT, loc)} ${endM.year}"
|
||||||
val m2 = endM.month.getDisplayName(TextStyle.SHORT_STANDALONE, loc).replaceFirstChar { it.uppercase(loc) }
|
|
||||||
"$m1 ${date.year} – $m2 ${endM.year}"
|
|
||||||
}
|
}
|
||||||
CalViewType.WEEK -> {
|
CalViewType.WEEK -> {
|
||||||
val start = date
|
val start = date
|
||||||
|
|||||||
@@ -64,9 +64,24 @@ class CalendarViewModel @Inject constructor(
|
|||||||
private var allCachedEvents: List<CalEvent> = emptyList()
|
private var allCachedEvents: List<CalEvent> = emptyList()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadVisible()
|
|
||||||
loadWritableCalendars()
|
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 {
|
private fun initialState(): CalendarUiState {
|
||||||
@@ -188,15 +203,6 @@ class CalendarViewModel @Inject constructor(
|
|||||||
_ready.value = true
|
_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 {
|
private fun isCached(start: Instant, end: Instant): Boolean {
|
||||||
val cs = cachedStart ?: return false
|
val cs = cachedStart ?: return false
|
||||||
val ce = cachedEnd ?: return false
|
val ce = cachedEnd ?: return false
|
||||||
@@ -231,8 +237,7 @@ class CalendarViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun syncWithServer() {
|
fun syncWithServer() {
|
||||||
invalidateCache()
|
invalidateCache()
|
||||||
loadVisible(force = true)
|
initialLoad()
|
||||||
prefetchBackground()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearError() = _state.update { it.copy(error = null) }
|
fun clearError() = _state.update { it.copy(error = null) }
|
||||||
@@ -280,8 +285,7 @@ class CalendarViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun afterMutation() {
|
private fun afterMutation() {
|
||||||
invalidateCache()
|
invalidateCache()
|
||||||
loadVisible(force = true)
|
initialLoad()
|
||||||
prefetchBackground()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveEvent(
|
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:statusBarColor">@android:color/black</item>
|
||||||
<item name="android:navigationBarColor">@android:color/black</item>
|
<item name="android:navigationBarColor">@android:color/black</item>
|
||||||
<item name="android:windowLightStatusBar">false</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>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
<item name="android:windowLightStatusBar">false</item>
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
</style>
|
</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">
|
<style name="Theme.Calendarr.Starting" parent="Theme.SplashScreen">
|
||||||
<item name="windowSplashScreenBackground">@android:color/black</item>
|
<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>
|
<item name="postSplashScreenTheme">@style/Theme.Calendarr</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user