Skip to content

feat: redesign Android UI with Neural Expressive direction - #1

Closed
olavbg wants to merge 36 commits into
mainfrom
design/neural-expressive-makeover
Closed

olavbg wants to merge 36 commits into
mainfrom
design/neural-expressive-makeover

Conversation

@olavbg

@olavbg olavbg commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Overview

This PR delivers the JavaZone Android UI makeover around a compact, developer-focused conference experience inspired by Google's Neural Expressive and Material 3 Expressive direction.

Design goals

  • Remove the bulky bottom navigation from the primary experience.
  • Treat the selected year as global context via an interactive JavaZone <year> app-bar control.
  • Keep the timeline as the fastest way to understand what is happening now, next, and in parallel.
  • Establish a deliberate JavaZone visual identity with semantic expressive accents.
  • Reduce header/filter chrome while preserving search, filtering, favorites, settings and historical programme access.
  • Improve visual hierarchy and accessibility rather than adding decoration for its own sake.

Implemented

  • Semantic JavaZone color tokens and refreshed light/dark Material 3 theme.
  • Compact app bar with interactive year picker; Archive is no longer a primary navigation destination.
  • Historical programme loading directly from the timeline.
  • Timeline day tabs are derived from the selected programme instead of being hard-coded to 2026.
  • Compact filter/search treatment and redesigned session cards with parallel-session grouping and live state.
  • Historical session details are routable with the programme year preserved.
  • Session detail screen redesigned around a stronger hero hierarchy, metadata card, live/upcoming/finished states, speakers and video actions.
  • Historical programmes are read-only for favorites, avoiding misleading favorite controls on archived sessions.
  • Accessibility-oriented content descriptions, state contrast and compact interaction targets retained throughout.

Verification status

The branch is intentionally still a draft. GitHub currently reports no workflow runs for the latest commit, so the final Android build/test verification still needs to be run in CI or locally before marking this ready for review.

@olavbg

olavbg commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

Hvordan ligger du an med denne? Du kan gjerne fullføre PR'en.

@olavbg
olavbg requested a lite review from Copilot September 8, 2026 18:26
@olavbg
olavbg marked this pull request as ready for review September 8, 2026 18:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

There are confirmed functional issues in session detail loading and timeline grouping/progress calculations that can lead to incorrect UI states or misleading presentation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR implements a major Android UI/navigation redesign aligned with a “Neural Expressive / Material 3 Expressive” direction, centered around making the timeline the primary surface and treating programme year as global context (replacing the old Archive-first navigation).

Changes:

  • Removes bottom navigation and introduces an app-bar year picker (bottom sheet) to switch between current and historical programmes.
  • Refactors timeline state to be year-aware and derives available day tabs from the selected programme.
  • Refreshes Material 3 theming with JavaZone semantic color tokens and a denser typography scale; adds a GitHub Action to compile the Android app on PRs.
File summaries
File Description
docs/design/neural-expressive-makeover.md Adds a design spec describing goals, IA, and acceptance criteria for the makeover.
app/src/main/java/com/olavbg/javazone/ui/timeline/TimelineViewModel.kt Introduces selected year state, historical loading, and derived day options for the timeline.
app/src/main/java/com/olavbg/javazone/ui/timeline/TimelineScreen.kt Rebuilds the timeline UI with compact app bar, search/filter affordances, day tabs, and updated session cards.
app/src/main/java/com/olavbg/javazone/ui/theme/Type.kt Defines a compact, high-contrast typography scale for dense conference content.
app/src/main/java/com/olavbg/javazone/ui/theme/Theme.kt Updates theme color schemes and removes dynamic-color usage to preserve JavaZone identity.
app/src/main/java/com/olavbg/javazone/ui/theme/Color.kt Introduces semantic JavaZone color tokens and keeps legacy aliases for incremental migration.
app/src/main/java/com/olavbg/javazone/ui/navigation/NavDestinations.kt Updates routing to preserve year in SessionDetail; removes Archive destination.
app/src/main/java/com/olavbg/javazone/ui/JavaZoneApp.kt Updates navigation flow: timeline → year picker sheet; session detail now carries year.
app/src/main/java/com/olavbg/javazone/ui/detail/SessionDetailScreen.kt Makes session details year-aware and disables favorites for archived years.
app/src/main/java/com/olavbg/javazone/ui/components/YearPicker.kt Adds a bottom-sheet year picker component with radio semantics.
.github/workflows/android-build.yml Adds CI workflow to build the debug APK on PRs / manual dispatch.
Review details
  • Files reviewed: 11/11 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +58 to 63
LaunchedEffect(sessionId, year) {
loading = true
session = if (isCurrentYear) repository.getSessions().first().find { it.id == sessionId }
else repository.getArchiveSessions(year).find { it.id == sessionId }
loading = false
}
private fun formatTime(zulu: String): String = try { java.time.Instant.parse(zulu).atZone(java.time.ZoneId.of("Europe/Oslo")).toLocalTime().toString().take(5) } catch (_: Exception) { "--:--" }
private fun isActive(s: Session, now: Instant) = try { now >= Instant.parse(s.startTimeZulu) && now < Instant.parse(s.endTimeZulu) } catch (_: Exception) { false }
private fun isPast(s: Session, now: Instant) = try { now > Instant.parse(s.endTimeZulu) } catch (_: Exception) { false }
private fun progress(s: Session, now: Instant): Float = try { val a=Instant.parse(s.startTimeZulu); val b=Instant.parse(s.endTimeZulu); (Duration.between(a, now).toMillis().toFloat()/Duration.between(a,b).toMillis()).coerceIn(0f,1f) } catch (_: Exception) { 0f }
Comment on lines +78 to +79
val groupedSessions = sessions.map { list -> list.sortedWith(compareBy<Session> { it.startTimeZulu }.thenBy { extractRoomNumber(it.room) }.thenBy { it.room }).groupBy { formatTime(it.startTimeZulu) } }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
Comment on lines +12 to +15
data class SessionDetail(
val sessionId: String,
val year: Int = 2026,
) : NavDestination

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5b0ee7d094

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

LaunchedEffect(offset) { simulatedTime = Instant.now().plusMillis(offset) }
LaunchedEffect(sessionId, year) {
loading = true
session = if (isCurrentYear) repository.getSessions().first().find { it.id == sessionId }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep current-programme detail sessions observable

For current-year detail views, first() captures the Room database's initial snapshot and then stops observing it. A deep link opened before the timeline refresh populates an empty database will therefore remain on “not found”, and favorite writes are never reflected in session, so the heart does not update and a second press still attempts to favorite rather than unfavorite. Continue collecting the session flow as before instead of taking a one-time value.

Useful? React with 👍 / 👎.


LaunchedEffect(offset) {
simulatedTime = Instant.now().plusMillis(offset)
LaunchedEffect(offset) { simulatedTime = Instant.now().plusMillis(offset) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh detail status time after resume

When a user opens a session detail, backgrounds the app through a schedule transition, and returns without changing the simulated-time setting, simulatedTime remains the instant from when the screen was first composed. The live/upcoming/finished banner and remaining-minute text can consequently remain stale indefinitely; restore the resume observer or update this state from a ticking/current-time flow.

Useful? React with 👍 / 👎.

_selectedFormat.value = null; _selectedLanguage.value = null; _selectedRoom.value = null; _filterSpeaker.value = null; _searchQuery.value = ""; hasScrolledForContext = false
if (year != CURRENT_YEAR) loadHistoricalYear(year)
}
private fun loadHistoricalYear(year: Int) { viewModelScope.launch { _isLoading.value = true; _yearSessions.value = repository.getArchiveSessions(year); _isLoading.value = false } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bind archive responses to the selected year

Selecting one historical year and then another before the first API call completes starts two independent coroutines that share _yearSessions. If the older request completes last, it overwrites the newer result while _selectedYear still names the second year, so the timeline can permanently show the wrong programme under that year label. Cancel the prior load or retain results by year and only publish the response that matches the current selection.

Useful? React with 👍 / 👎.

@olavbg olavbg closed this Sep 8, 2026
@olavbg

olavbg commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

Ga ikke ønsket forbedring i design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants