| 1 |
package io.oxygenhub.android.ui.dashboard |
| 2 |
|
| 3 |
import androidx.compose.foundation.clickable |
| 4 |
import androidx.compose.foundation.layout.Arrangement |
| 5 |
import androidx.compose.foundation.layout.BoxWithConstraints |
| 6 |
import androidx.compose.foundation.layout.Column |
| 7 |
import androidx.compose.foundation.layout.Row |
| 8 |
import androidx.compose.foundation.layout.Spacer |
| 9 |
import androidx.compose.foundation.layout.fillMaxWidth |
| 10 |
import androidx.compose.foundation.layout.height |
| 11 |
import androidx.compose.foundation.layout.padding |
| 12 |
import androidx.compose.foundation.rememberScrollState |
| 13 |
import androidx.compose.foundation.verticalScroll |
| 14 |
import androidx.compose.material.icons.Icons |
| 15 |
import androidx.compose.material.icons.outlined.Inbox |
| 16 |
import androidx.compose.material.icons.outlined.MarkEmailUnread |
| 17 |
import androidx.compose.material3.Card |
| 18 |
import androidx.compose.material3.HorizontalDivider |
| 19 |
import androidx.compose.material3.Icon |
| 20 |
import androidx.compose.material3.ListItem |
| 21 |
import androidx.compose.material3.ListItemDefaults |
| 22 |
import androidx.compose.material3.MaterialTheme |
| 23 |
import androidx.compose.material3.Text |
| 24 |
import androidx.compose.material3.TextButton |
| 25 |
import androidx.compose.runtime.Composable |
| 26 |
import androidx.compose.runtime.getValue |
| 27 |
import androidx.compose.ui.Modifier |
| 28 |
import androidx.compose.ui.graphics.Color |
| 29 |
import androidx.compose.ui.text.font.FontWeight |
| 30 |
import androidx.compose.ui.unit.dp |
| 31 |
import androidx.lifecycle.compose.collectAsStateWithLifecycle |
| 32 |
import io.oxygenhub.android.api.Inbox |
| 33 |
import io.oxygenhub.android.api.Repository |
| 34 |
import io.oxygenhub.android.ui.common.AppScreen |
| 35 |
import io.oxygenhub.android.ui.common.Load |
| 36 |
import io.oxygenhub.android.ui.common.LoadContent |
| 37 |
import io.oxygenhub.android.ui.repos.RepositoryRow |
| 38 |
|
| 39 |
@Composable |
| 40 |
fun DashboardScreen( |
| 41 |
vm: DashboardViewModel, |
| 42 |
hub: String, |
| 43 |
inbox: Load<Inbox>, |
| 44 |
onRefreshInbox: () -> Unit, |
| 45 |
onOpenRepository: (Repository) -> Unit, |
| 46 |
onAllRepositories: () -> Unit, |
| 47 |
onInbox: () -> Unit, |
| 48 |
) { |
| 49 |
val state by vm.state.collectAsStateWithLifecycle() |
| 50 |
AppScreen(title = "Dashboard", subtitle = hub.substringAfter("://")) { |
| 51 |
LoadContent(state, onRefresh = { vm.refresh(); onRefreshInbox() }) { d -> |
| 52 |
DashboardContent(d, inbox, onOpenRepository, onAllRepositories, onInbox) |
| 53 |
} |
| 54 |
} |
| 55 |
} |
| 56 |
|
| 57 |
@Composable |
| 58 |
fun DashboardContent( |
| 59 |
d: Dashboard, |
| 60 |
inbox: Load<Inbox>, |
| 61 |
onOpenRepository: (Repository) -> Unit, |
| 62 |
onAllRepositories: () -> Unit, |
| 63 |
onInbox: () -> Unit, |
| 64 |
) { |
| 65 |
BoxWithConstraints { |
| 66 |
// Side by side once there is room (a tablet, or a foldable open). |
| 67 |
val wide = maxWidth >= 720.dp |
| 68 |
Column( |
| 69 |
Modifier |
| 70 |
.fillMaxWidth() |
| 71 |
.verticalScroll(rememberScrollState()) |
| 72 |
.padding(16.dp), |
| 73 |
) { |
| 74 |
Text(d.greeting, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold) |
| 75 |
Spacer(Modifier.height(16.dp)) |
| 76 |
if (wide) { |
| 77 |
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { |
| 78 |
Column(Modifier.weight(2f)) { Recent(d, onOpenRepository, onAllRepositories) } |
| 79 |
Column(Modifier.weight(1f)) { Unread(inbox, onInbox) } |
| 80 |
} |
| 81 |
} else { |
| 82 |
Unread(inbox, onInbox) |
| 83 |
Spacer(Modifier.height(16.dp)) |
| 84 |
Recent(d, onOpenRepository, onAllRepositories) |
| 85 |
} |
| 86 |
} |
| 87 |
} |
| 88 |
} |
| 89 |
|
| 90 |
@Composable |
| 91 |
private fun Unread(inbox: Load<Inbox>, onInbox: () -> Unit) { |
| 92 |
Card(Modifier.fillMaxWidth().clickable(onClick = onInbox)) { |
| 93 |
ListItem( |
| 94 |
colors = ListItemDefaults.colors(containerColor = Color.Transparent), |
| 95 |
leadingContent = { |
| 96 |
Icon(if ((inbox.valueOrNull?.unread ?: 0) > 0) Icons.Outlined.MarkEmailUnread else Icons.Outlined.Inbox, contentDescription = null) |
| 97 |
}, |
| 98 |
headlineContent = { |
| 99 |
Text( |
| 100 |
when (inbox) { |
| 101 |
Load.Loading -> "Inbox" |
| 102 |
is Load.Failed -> "Inbox unavailable" |
| 103 |
is Load.Ready -> when (val n = inbox.value.unread) { |
| 104 |
0L -> "Nothing unread" |
| 105 |
1L -> "1 unread notification" |
| 106 |
else -> "$n unread notifications" |
| 107 |
} |
| 108 |
}, |
| 109 |
) |
| 110 |
}, |
| 111 |
supportingContent = (inbox as? Load.Failed)?.let { f -> { Text(f.message) } }, |
| 112 |
) |
| 113 |
} |
| 114 |
} |
| 115 |
|
| 116 |
@Composable |
| 117 |
private fun Recent(d: Dashboard, onOpenRepository: (Repository) -> Unit, onAllRepositories: () -> Unit) { |
| 118 |
Card(Modifier.fillMaxWidth()) { |
| 119 |
Text( |
| 120 |
if (d.recentAreYours) "Your recent repositories" else "Recently updated on the hub", |
| 121 |
style = MaterialTheme.typography.titleMedium, |
| 122 |
modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp), |
| 123 |
) |
| 124 |
if (!d.recentAreYours) { |
| 125 |
Text( |
| 126 |
"You have no repositories of your own yet.", |
| 127 |
style = MaterialTheme.typography.bodySmall, |
| 128 |
color = MaterialTheme.colorScheme.onSurfaceVariant, |
| 129 |
modifier = Modifier.padding(horizontal = 16.dp), |
| 130 |
) |
| 131 |
} |
| 132 |
if (d.recent.isEmpty()) { |
| 133 |
Text("No repositories to show.", modifier = Modifier.padding(16.dp)) |
| 134 |
} |
| 135 |
d.recent.forEachIndexed { i, repo -> |
| 136 |
if (i > 0) HorizontalDivider() |
| 137 |
RepositoryRow(repo, onClick = { onOpenRepository(repo) }) |
| 138 |
} |
| 139 |
TextButton(onClick = onAllRepositories, modifier = Modifier.padding(8.dp)) { Text("All repositories") } |
| 140 |
} |
| 141 |
} |