OXYGENHUB

The unfolded rail: Projects and Search when there is room (closes #14) 6fcaca2bd500

Opened, the Fold's rail (and a tablet's) adds Projects and Search to the
four; a phone's bar keeps the four, and the dashboard offers Projects and
Search as buttons.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 3867a40bc3ea
When
· 2026-10-25 23:54 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
6fcaca2bd500f003313c22827469cea3dac7f907c7e0f6960ad8ae36d2f1d36d
Depends on
144cbf7bf2b5 1c42a70fcba3 dfbc3e972872 e833eb0a9e7f
Depended on by
821035db5a1c
In bookmarks
main
In tags
v0.1.0 v0.1.1 v0.1.2 v0.1.3 v0.1.4 v0.1.5 v0.1.6 v0.1.7 v0.1.8 v0.1.9
Edits
13 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified app/src/main/java/io/oxygenhub/android/ui/dashboard/DashboardScreen.kt +6 −0
…
16 unchanged lines
17 17
import androidx.compose.material.icons.outlined.Inbox
18 18
import androidx.compose.material.icons.outlined.MarkEmailUnread
19 19
import androidx.compose.material.icons.outlined.Search
20 +
import androidx.compose.material.icons.outlined.ViewKanban
20 21
import androidx.compose.material3.Card
21 22
import androidx.compose.material3.HorizontalDivider
22 23
import androidx.compose.material3.Icon
…
34 unchanged lines
57 58
    onInbox: () -> Unit,
58 59
    onOpenProposal: (ProposalBrief) -> Unit = {},
59 60
    onSearch: (() -> Unit)? = null,
61 +
    /** Projects, when the bar has no room for it (a phone, #14). */
62 +
    onProjects: (() -> Unit)? = null,
60 63
) {
61 64
    val state by vm.state.collectAsStateWithLifecycle()
62 65
    AppScreen(
63 66
        title = "Dashboard",
64 67
        subtitle = hub.substringAfter("://"),
65 68
        actions = {
69 +
            if (onProjects != null) {
70 +
                IconButton(onClick = onProjects) { Icon(Icons.Outlined.ViewKanban, contentDescription = "Projects") }
71 +
            }
66 72
            if (onSearch != null) {
67 73
                IconButton(onClick = onSearch) { Icon(Icons.Outlined.Search, contentDescription = "Search issues and proposals") }
68 74
            }
…
143 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/nav/AppNavigation.kt +22 −3
…
4 unchanged lines
5 5
import androidx.compose.material.icons.filled.Book
6 6
import androidx.compose.material.icons.filled.Dashboard
7 7
import androidx.compose.material.icons.filled.Inbox
8 +
import androidx.compose.material.icons.filled.Search
8 9
import androidx.compose.material.icons.filled.ViewKanban
9 10
import androidx.compose.material.icons.outlined.AccountCircle
10 11
import androidx.compose.material.icons.outlined.Book
11 12
import androidx.compose.material.icons.outlined.Dashboard
12 13
import androidx.compose.material.icons.outlined.Inbox
14 +
import androidx.compose.material.icons.outlined.Search
13 15
import androidx.compose.material.icons.outlined.ViewKanban
14 16
import androidx.compose.material3.Badge
15 17
import androidx.compose.material3.BadgedBox
16 18
import androidx.compose.material3.Icon
17 19
import androidx.compose.material3.Text
20 +
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
18 21
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
19 22
import androidx.compose.runtime.Composable
20 23
import androidx.compose.runtime.LaunchedEffect
…
14 unchanged lines
35 38
import androidx.navigation.compose.currentBackStackEntryAsState
36 39
import androidx.navigation.compose.rememberNavController
37 40
import androidx.navigation.toRoute
41 +
import androidx.window.core.layout.WindowSizeClass
38 42
import io.oxygenhub.android.api.HubApi
39 43
import io.oxygenhub.android.auth.Session
40 44
import io.oxygenhub.android.ui.account.AccountScreen
…
79 unchanged lines
120 124
121 125
@Serializable data class JobLogRoute(val owner: String, val name: String, val number: Long, val job: String)
122 126
123 -
enum class TopLevel(val label: String, val icon: ImageVector, val selectedIcon: ImageVector, val route: Any, val routeClass: KClass<*>) {
127 +
/**
128 +
 * Where the bar or rail goes. [wide] ones show only when there is room (the
129 +
 * unfolded Fold, a tablet: #14); on a phone the bar keeps its four and they
130 +
 * are reached from the dashboard.
131 +
 */
132 +
enum class TopLevel(
133 +
    val label: String,
134 +
    val icon: ImageVector,
135 +
    val selectedIcon: ImageVector,
136 +
    val route: Any,
137 +
    val routeClass: KClass<*>,
138 +
    val wide: Boolean = false,
139 +
) {
124 140
    Dashboard("Dashboard", Icons.Outlined.Dashboard, Icons.Filled.Dashboard, DashboardRoute, DashboardRoute::class),
125 141
    Repositories("Repositories", Icons.Outlined.Book, Icons.Filled.Book, RepositoriesRoute, RepositoriesRoute::class),
126 -
    Projects("Projects", Icons.Outlined.ViewKanban, Icons.Filled.ViewKanban, ProjectsRoute, ProjectsRoute::class),
142 +
    Projects("Projects", Icons.Outlined.ViewKanban, Icons.Filled.ViewKanban, ProjectsRoute, ProjectsRoute::class, wide = true),
143 +
    Search("Search", Icons.Outlined.Search, Icons.Filled.Search, SearchRoute, SearchRoute::class, wide = true),
127 144
    Inbox("Inbox", Icons.Outlined.Inbox, Icons.Filled.Inbox, InboxRoute, InboxRoute::class),
128 145
    Account("Account", Icons.Outlined.AccountCircle, Icons.Filled.AccountCircle, AccountRoute, AccountRoute::class),
129 146
}
…
25 unchanged lines
155 172
    val entry by nav.currentBackStackEntryAsState()
156 173
    val destination = entry?.destination
157 174
175 +
    val roomy = currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
158 176
    NavigationSuiteScaffold(
159 177
        navigationSuiteItems = {
160 -
            TopLevel.entries.forEach { top ->
178 +
            TopLevel.entries.filter { roomy || !it.wide }.forEach { top ->
161 179
                val selected = destination?.hierarchy?.any { it.hasRoute(top.routeClass) } == true
162 180
                item(
163 181
                    selected = selected,
…
25 unchanged lines
189 207
                    onInbox = { nav.goTo(TopLevel.Inbox) },
190 208
                    onOpenProposal = { nav.navigate(ProposalRoute(it.owner, it.name, it.number)) },
191 209
                    onSearch = { nav.navigate(SearchRoute) },
210 +
                    onProjects = if (roomy) null else { { nav.navigate(ProjectsRoute) } },
192 211
                )
193 212
            }
194 213
            composable<SearchRoute> {
…
136 unchanged lines