Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b2f8415
docs: add Neural Expressive redesign specification
olavbg Sep 7, 2026
36fcb9f
feat: remove persistent bottom navigation
olavbg Sep 7, 2026
ae8de90
refactor: establish expressive JavaZone color tokens
olavbg Sep 7, 2026
bb19691
refactor: apply expressive JavaZone theme
olavbg Sep 7, 2026
8efbfc4
refactor: add compact expressive typography scale
olavbg Sep 7, 2026
b6a36f6
feat: redesign timeline and year-aware schedule navigation
olavbg Sep 8, 2026
e6555b0
tmp
olavbg Sep 8, 2026
57a6549
chore: remove temporary timeline viewmodel file
olavbg Sep 8, 2026
b3d0ebc
refactor: replace timeline viewmodel with year-aware implementation
olavbg Sep 8, 2026
ed49e85
feat: add year-aware timeline state and historical programme loading
olavbg Sep 8, 2026
ead15a7
feat: add expressive year picker sheet
olavbg Sep 8, 2026
45c4a0e
refactor: integrate year picker into primary navigation
olavbg Sep 8, 2026
cc2113a
feat: replace archive navigation with year picker
olavbg Sep 8, 2026
1ed2ed4
refactor: remove archive navigation destination
olavbg Sep 8, 2026
2270573
refactor: remove archive navigation destination
olavbg Sep 8, 2026
04b0dbc
fix: derive timeline days from selected programme
olavbg Sep 8, 2026
9c2e16f
fix: support historical day tabs and disable archived favorites
olavbg Sep 8, 2026
878b303
fix: preserve programme year when opening session details
olavbg Sep 8, 2026
b904f4b
fix: route session details with selected programme year
olavbg Sep 8, 2026
598a8ca
feat: redesign session details and support historical programmes
olavbg Sep 8, 2026
15ceac3
refactor: polish session detail hierarchy and metadata
olavbg Sep 8, 2026
2b6083c
fix: resolve SessionDetail compile issue
olavbg Sep 8, 2026
de08a35
fix: clear speaker filter with other filters
olavbg Sep 8, 2026
fef1ea4
refine: expressive accessible year picker
olavbg Sep 8, 2026
4b0de22
ci: add Android build verification workflow
olavbg Sep 8, 2026
7b0928b
fix: run Gradle wrapper through bash in CI
olavbg Sep 8, 2026
d1b4788
fix: import rememberSaveable in app root
olavbg Sep 8, 2026
1efe879
fix: compile timeline rememberSaveable and trailing icon
olavbg Sep 8, 2026
5b0ee7d
fix: opt in to experimental Material3 detail API
olavbg Sep 8, 2026
1fae735
refactor: centralize JavaZone year configuration
olavbg Sep 8, 2026
73cfd18
refactor: use centralized current year in navigation
olavbg Sep 8, 2026
8bd9f04
fix: keep timeline groups separated by conference day
olavbg Sep 8, 2026
2100a11
fix: guard timeline progress calculation
olavbg Sep 8, 2026
b8d8ccf
fix: react to current session data updates on detail screen
olavbg Sep 8, 2026
1617aa4
refactor: use centralized year range in app navigation
olavbg Sep 8, 2026
803e063
fix: allow detail screen to recover from initially empty session flow
olavbg Sep 8, 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
31 changes: 31 additions & 0 deletions .github/workflows/android-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Android build verification

on:
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
build:
name: Compile Android app
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: gradle

- name: Set up Android SDK
uses: android-actions/setup-android@v3

- name: Build debug APK
run: bash ./gradlew assembleDebug --stacktrace
6 changes: 6 additions & 0 deletions app/src/main/java/com/olavbg/javazone/JavaZoneConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.olavbg.javazone

object JavaZoneConfig {
const val CURRENT_YEAR = 2026
const val FIRST_ARCHIVE_YEAR = 2014
}
107 changes: 30 additions & 77 deletions app/src/main/java/com/olavbg/javazone/ui/JavaZoneApp.kt
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
package com.olavbg.javazone.ui

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.scene.SinglePaneSceneStrategy
import androidx.navigation3.ui.NavDisplay
import com.olavbg.javazone.JavaZoneConfig
import com.olavbg.javazone.data.repository.SessionRepository
import com.olavbg.javazone.data.repository.SettingsRepository
import com.olavbg.javazone.notifications.ReminderManager
import com.olavbg.javazone.ui.archive.ArchiveScreen
import com.olavbg.javazone.ui.archive.ArchiveViewModelFactory
import com.olavbg.javazone.ui.components.YearPickerSheet
import com.olavbg.javazone.ui.detail.SessionDetailScreen
import com.olavbg.javazone.ui.navigation.NavDestination
import com.olavbg.javazone.ui.settings.SettingsScreen
Expand All @@ -30,84 +22,43 @@ import com.olavbg.javazone.ui.timeline.TimelineScreen
import com.olavbg.javazone.ui.timeline.TimelineViewModel
import com.olavbg.javazone.ui.timeline.TimelineViewModelFactory

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun JavaZoneApp(
repository: SessionRepository,
settingsRepository: SettingsRepository,
reminderManager: ReminderManager,
initialSessionId: String? = null,
) {
val timelineViewModel: TimelineViewModel = viewModel(
factory = TimelineViewModelFactory(repository, settingsRepository)
)
val timelineViewModel: TimelineViewModel = viewModel(factory = TimelineViewModelFactory(repository, settingsRepository))
var yearPickerVisible by rememberSaveable { mutableStateOf(false) }
val years = remember { (JavaZoneConfig.FIRST_ARCHIVE_YEAR..JavaZoneConfig.CURRENT_YEAR).toList().reversed() }

val backStack = if (initialSessionId != null) {
rememberNavBackStack(
NavDestination.Timeline, NavDestination.SessionDetail(initialSessionId)
)
rememberNavBackStack(NavDestination.Timeline, NavDestination.SessionDetail(initialSessionId))
} else {
rememberNavBackStack(NavDestination.Timeline)
}

Scaffold(
bottomBar = {
val currentEntry = backStack.lastOrNull()
NavigationBar {
NavigationBarItem(
selected = currentEntry == NavDestination.Timeline,
onClick = {
if (currentEntry != NavDestination.Timeline) {
backStack.clear()
backStack.add(NavDestination.Timeline)
}
},
icon = { Icon(Icons.Default.Schedule, contentDescription = "Tidslinje") },
label = { Text("Tidslinje") })
NavigationBarItem(
selected = currentEntry == NavDestination.Archive,
onClick = {
if (currentEntry != NavDestination.Archive) {
backStack.clear()
backStack.add(NavDestination.Archive)
}
},
icon = { Icon(Icons.Default.History, contentDescription = "Arkiv") },
label = { Text("Arkiv") })
}
}) { innerPadding ->
Scaffold { innerPadding ->
NavDisplay(
backStack = backStack,
sceneStrategies = listOf(SinglePaneSceneStrategy()),
modifier = Modifier.fillMaxSize(),
modifier = androidx.compose.ui.Modifier.fillMaxSize(),
entryProvider = { key ->
when (key) {
NavDestination.Timeline -> NavEntry(
key = key
) {
NavDestination.Timeline -> NavEntry(key = key) {
TimelineScreen(
viewModel = timelineViewModel, onSessionClick = { id ->
backStack.add(NavDestination.SessionDetail(id))
}, onSettingsClick = {
backStack.add(NavDestination.Settings)
}, contentPadding = innerPadding
)
}

NavDestination.Archive -> NavEntry(
key = key
) {
ArchiveScreen(
viewModel = viewModel(factory = ArchiveViewModelFactory(repository)),
viewModel = timelineViewModel,
onSessionClick = { id -> backStack.add(NavDestination.SessionDetail(id, timelineViewModel.selectedYear.value)) },
onSettingsClick = { backStack.add(NavDestination.Settings) },
onYearClick = { yearPickerVisible = true },
contentPadding = innerPadding
)
}

is NavDestination.SessionDetail -> NavEntry(
key = key
) {
is NavDestination.SessionDetail -> NavEntry(key = key) {
SessionDetailScreen(
sessionId = key.sessionId,
year = key.year,
repository = repository,
settingsRepository = settingsRepository,
onBackClick = { backStack.removeAt(backStack.size - 1) },
Expand All @@ -118,23 +69,25 @@ fun JavaZoneApp(
contentPadding = innerPadding
)
}

NavDestination.Settings -> NavEntry(
key = key
) {
NavDestination.Settings -> NavEntry(key = key) {
SettingsScreen(
viewModel = viewModel(
factory = SettingsViewModelFactory(
settingsRepository, repository, reminderManager
)
),
viewModel = viewModel(factory = SettingsViewModelFactory(settingsRepository, repository, reminderManager)),
onBackClick = { backStack.removeAt(backStack.size - 1) },
contentPadding = innerPadding
)
}

else -> NavEntry(key) { }
}
})
}
)
}

if (yearPickerVisible) {
YearPickerSheet(
selectedYear = timelineViewModel.selectedYear.collectAsState().value,
years = years,
onYearSelected = { year -> timelineViewModel.setYear(year); yearPickerVisible = false },
onDismiss = { yearPickerVisible = false },
)
}
}
79 changes: 79 additions & 0 deletions app/src/main/java/com/olavbg/javazone/ui/components/YearPicker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.olavbg.javazone.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.compose.ui.text.font.FontWeight

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun YearPickerSheet(
selectedYear: Int,
years: List<Int>,
onYearSelected: (Int) -> Unit,
onDismiss: () -> Unit,
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
) {
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Text("Velg JavaZone-år", style = MaterialTheme.typography.headlineSmall)
Text("Bytt program uten å forlate tidslinjen.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
LazyColumn(
contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(years, key = { it }) { year ->
val selected = year == selectedYear
Surface(
onClick = { onYearSelected(year) },
shape = MaterialTheme.shapes.large,
color = if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerLow,
tonalElevation = if (selected) 2.dp else 0.dp,
modifier = Modifier.fillMaxWidth().semantics {
role = Role.RadioButton
this.selected = selected
},
) {
Row(Modifier.padding(horizontal = 18.dp, vertical = 15.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
text = year.toString(),
style = if (selected) MaterialTheme.typography.titleLarge else MaterialTheme.typography.titleMedium,
fontWeight = if (selected) FontWeight.ExtraBold else FontWeight.Bold,
color = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f),
)
if (selected) Icon(Icons.Default.Check, contentDescription = "Valgt", tint = MaterialTheme.colorScheme.onPrimaryContainer)
}
}
}
}
}
}
}
Loading
Loading