You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
403 lines
11 KiB
403 lines
11 KiB
package androidx.ui.examples.jetissues.view |
|
|
|
import androidx.compose.foundation.* |
|
import androidx.ui.examples.jetissues.view.common.WithoutSelection |
|
import androidx.compose.foundation.layout.* |
|
import androidx.compose.foundation.shape.RoundedCornerShape |
|
import androidx.compose.material.* |
|
import androidx.compose.runtime.* |
|
import androidx.compose.ui.Alignment |
|
import androidx.compose.ui.Modifier |
|
import androidx.compose.ui.graphics.Color |
|
import androidx.compose.ui.graphics.luminance |
|
import androidx.compose.ui.layout.WithConstraints |
|
import androidx.compose.ui.platform.DensityAmbient |
|
import androidx.compose.ui.text.AnnotatedString |
|
import androidx.compose.ui.text.SpanStyle |
|
import androidx.compose.ui.text.TextStyle |
|
import androidx.compose.ui.text.font.FontStyle |
|
import androidx.compose.ui.text.font.FontWeight |
|
import androidx.compose.ui.unit.dp |
|
import androidx.ui.examples.jetissues.data.* |
|
import androidx.ui.examples.jetissues.query.IssueQuery |
|
import androidx.ui.examples.jetissues.query.IssuesQuery |
|
import androidx.ui.examples.jetissues.query.type.OrderDirection |
|
import androidx.ui.examples.jetissues.view.common.SelectionContainer |
|
import org.ocpsoft.prettytime.PrettyTime |
|
import java.lang.Integer.parseInt |
|
import java.util.* |
|
|
|
val Repository = ambientOf<IssuesRepository>() |
|
|
|
@Composable |
|
fun JetIssuesView() { |
|
MaterialTheme( |
|
colors = lightThemeColors |
|
) { |
|
WithoutSelection { |
|
Main() |
|
} |
|
} |
|
|
|
} |
|
|
|
@Composable |
|
fun Main() { |
|
val currentIssue: MutableState<IssuesQuery.Node?> = remember { mutableStateOf(null) } |
|
WithConstraints { |
|
if (maxWidth.value > 1000) { |
|
TwoColumnsLayout(currentIssue) |
|
} else { |
|
SingleColumnLayout(currentIssue) |
|
} |
|
} |
|
|
|
} |
|
|
|
@Composable |
|
fun SingleColumnLayout(currentIssue: MutableState<IssuesQuery.Node?>) { |
|
val issue = currentIssue.value |
|
if(issue == null) { |
|
IssuesList(currentIssue) |
|
} else { |
|
Column { |
|
Scaffold( |
|
topBar = { |
|
TopAppBar( |
|
title = { |
|
Text( |
|
text = "#${issue.number}", |
|
style = MaterialTheme.typography.h5 |
|
) |
|
}, |
|
navigationIcon = { |
|
Button(onClick = { |
|
currentIssue.value = null |
|
}) { |
|
Text(text = "Back") |
|
} |
|
} |
|
) |
|
}, |
|
bodyContent = { |
|
CurrentIssue(currentIssue.value) |
|
} |
|
) |
|
} |
|
} |
|
} |
|
|
|
@Composable |
|
fun TwoColumnsLayout(currentIssue: MutableState<IssuesQuery.Node?>) { |
|
Row(Modifier.fillMaxSize()) { |
|
Box(modifier = Modifier.fillMaxWidth(0.4f), alignment = Alignment.Center) { |
|
IssuesList(currentIssue) |
|
} |
|
CurrentIssue(currentIssue.value) |
|
} |
|
} |
|
|
|
@Composable |
|
fun CurrentIssue( |
|
issue: IssuesQuery.Node? |
|
) { |
|
when (issue) { |
|
null -> CurrentIssueStatus { Text("Select issue") } |
|
else -> { |
|
val repo = Repository.current |
|
val issueBody = uiStateFrom(issue.number) { clb: (Result<IssueQuery.Issue>) -> Unit -> |
|
repo.getIssue(issue.number, callback = clb) |
|
}.value |
|
when (issueBody) { |
|
is UiState.Loading -> CurrentIssueStatus { Loader() } |
|
is UiState.Error -> CurrentIssueStatus { Error("Issue loading error") } |
|
is UiState.Success -> CurrentIssueActive(issue, issueBody.data) |
|
} |
|
} |
|
} |
|
} |
|
|
|
@Composable |
|
fun CurrentIssueStatus(content: @Composable () -> Unit) { |
|
Box(modifier = Modifier.fillMaxSize(), gravity = ContentGravity.Center) { |
|
content() |
|
} |
|
} |
|
|
|
@Composable |
|
fun CurrentIssueActive(issue: IssuesQuery.Node, body: IssueQuery.Issue) { |
|
ScrollableColumn(modifier = Modifier.padding(15.dp).fillMaxSize()) { |
|
SelectionContainer { |
|
Text( |
|
text = issue.title, |
|
style = MaterialTheme.typography.h5 |
|
) |
|
} |
|
|
|
Row(horizontalArrangement = Arrangement.Center) { |
|
CreatedBy(issue) |
|
} |
|
|
|
Labels(issue.labels) |
|
|
|
Spacer(Modifier.height(8.dp)) |
|
|
|
SelectionContainer { |
|
Text( |
|
text = body.body, |
|
modifier = Modifier.padding(4.dp), |
|
style = MaterialTheme.typography.body1 |
|
) |
|
} |
|
} |
|
} |
|
|
|
@Composable |
|
fun IssuesList(currentIssue: MutableState<IssuesQuery.Node?>) { |
|
val scroll = rememberScrollState(0f) |
|
val issuesState = remember { mutableStateOf(IssuesState.OPEN) } |
|
val issuesOrder = remember { mutableStateOf(OrderDirection.DESC) } |
|
Column { |
|
Scaffold( |
|
topBar = { |
|
TopAppBar( |
|
title = { Text(text = "JetIssues") }, |
|
actions = { |
|
OrderButton(issuesOrder, scroll) |
|
} |
|
) |
|
}, |
|
bodyContent = { |
|
Column { |
|
FilterTabs(issuesState, scroll) |
|
ListBody( |
|
scroll, |
|
currentIssue = currentIssue, |
|
issuesState = issuesState.value, |
|
issuesOrder = issuesOrder.value |
|
) |
|
} |
|
} |
|
) |
|
} |
|
} |
|
|
|
@Composable |
|
fun OrderButton(order: MutableState<OrderDirection>, scroll: ScrollState) { |
|
when (order.value) { |
|
OrderDirection.DESC -> |
|
Button(onClick = { |
|
order.value = OrderDirection.ASC |
|
scroll.scrollTo(0F) |
|
}) { |
|
Text("ASC") |
|
} |
|
OrderDirection.ASC -> |
|
Button(onClick = { |
|
order.value = OrderDirection.DESC |
|
scroll.scrollTo(0F) |
|
}) { |
|
Text("DESC") |
|
} |
|
} |
|
} |
|
|
|
|
|
@Composable |
|
fun FilterTabs(issuesState: MutableState<IssuesState>, scroll: ScrollState) { |
|
TabRow(selectedTabIndex = IssuesState.values().toList().indexOf(issuesState.value)) { |
|
IssuesState.values().forEach { |
|
Tab( |
|
text = { Text(it.title) }, |
|
selected = issuesState.value == it, |
|
onClick = { |
|
issuesState.value = it |
|
scroll.scrollTo(0F) |
|
} |
|
) |
|
} |
|
} |
|
} |
|
|
|
@Composable |
|
fun ListBody( |
|
scroll: ScrollState, |
|
currentIssue: MutableState<IssuesQuery.Node?>, |
|
issuesState: IssuesState, |
|
issuesOrder: OrderDirection |
|
) { |
|
val repo = Repository.current |
|
val issues = uiStateFrom(issuesState, issuesOrder) { clb: (Result<Issues>) -> Unit -> |
|
repo.getIssues(issuesState, issuesOrder, callback = clb) |
|
} |
|
|
|
ScrollableColumn(scrollState = scroll) { |
|
issues.value.let { |
|
when (it) { |
|
is UiState.Success -> { |
|
for (iss in it.data.nodes) { |
|
Box(modifier = Modifier.clickable { |
|
currentIssue.value = iss |
|
}, alignment = Alignment.CenterStart) { |
|
ListItem(iss) |
|
} |
|
} |
|
MoreButton(issues) |
|
} |
|
|
|
is UiState.Loading -> Loader() |
|
is UiState.Error -> Error("Issues loading error") |
|
} |
|
} |
|
} |
|
} |
|
|
|
@Composable |
|
fun ListItem(x: IssuesQuery.Node) { |
|
Card(modifier = Modifier.padding(10.dp).fillMaxWidth()) { |
|
CardBody(x) |
|
} |
|
} |
|
|
|
@Composable |
|
fun CardBody(x: IssuesQuery.Node) { |
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { |
|
Column { |
|
Row { |
|
CreatedAt(x) |
|
Spacer(Modifier.width(10.dp)) |
|
Number(x) |
|
} |
|
|
|
Title(x) |
|
Labels(x.labels) |
|
} |
|
} |
|
} |
|
|
|
@Composable |
|
fun Title(x: IssuesQuery.Node) { |
|
Text(text = x.title) |
|
} |
|
|
|
private val timePrinter = PrettyTime() |
|
|
|
private val ISSUE_DATE_STYLE = TextStyle(color = Color.Gray, fontStyle = FontStyle.Italic) |
|
|
|
@Composable |
|
fun CreatedAt(x: IssuesQuery.Node) { |
|
Text(text = timePrinter.format(x.createdAt as Date), style = ISSUE_DATE_STYLE) |
|
} |
|
|
|
@Composable |
|
fun Number(x: IssuesQuery.Node) { |
|
Text(text = "#${x.number}") |
|
} |
|
|
|
@Composable |
|
fun CreatedBy(issue: IssuesQuery.Node) { |
|
val text = AnnotatedString.Builder().apply { |
|
pushStyle(ISSUE_DATE_STYLE.toSpanStyle()) |
|
append(timePrinter.format(issue.createdAt as Date)) |
|
pop() |
|
issue.author?.login?.let { |
|
append(" by ") |
|
pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) |
|
append(it) |
|
} |
|
}.toAnnotatedString() |
|
Text(text = text) |
|
} |
|
|
|
@Composable |
|
fun MoreButton(issues: MutableState<UiState<Issues>>) { |
|
val value = issues.value |
|
if (value !is UiState.Success) { |
|
return |
|
} |
|
val issuesData = value.data |
|
val cursor = issuesData.cursor |
|
if (cursor == null) { |
|
return |
|
} |
|
|
|
var loading by remember { mutableStateOf(false) } |
|
Box( |
|
gravity = ContentGravity.Center, |
|
modifier = Modifier.fillMaxWidth().padding(10.dp) |
|
) { |
|
if (loading) { |
|
Loader() |
|
} else { |
|
val repo = Repository.current |
|
Button(onClick = { |
|
loading = true |
|
repo.getIssues(issuesData.state, issuesData.order, cursor) { |
|
loading = false |
|
when (it) { |
|
is Result.Error -> issues.value = UiState.Error(it.exception) |
|
is Result.Success -> issues.value = UiState.Success(it.data.copy(nodes = issuesData.nodes + it.data.nodes)) |
|
} |
|
} |
|
}) { |
|
Text(text = "More") |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
@Composable |
|
fun Labels(labels: IssuesQuery.Labels?) { |
|
Row { |
|
labels?.nodes?.filterNotNull()?.forEach { |
|
val color = parseColor(it.color) |
|
val textColor = if (color.luminance() > 0.5) Color.Black else Color.White |
|
Box( |
|
shape = RoundedCornerShape(3.dp), |
|
modifier = Modifier.padding(3.dp), |
|
backgroundColor = color |
|
) { |
|
Text( |
|
text = it.name, |
|
modifier = Modifier.padding(3.dp), |
|
style = TextStyle(color = textColor) |
|
) |
|
} |
|
} |
|
} |
|
} |
|
|
|
@Composable |
|
fun Loader() { |
|
Box( |
|
gravity = ContentGravity.Center, |
|
modifier = Modifier.fillMaxWidth().padding(20.dp) |
|
) { |
|
CircularProgressIndicator() |
|
} |
|
} |
|
|
|
@Composable |
|
fun Error(err: String) { |
|
Box( |
|
gravity = ContentGravity.Center, |
|
modifier = Modifier.fillMaxWidth().padding(20.dp) |
|
) { |
|
Text(text = err, style = TextStyle(color = MaterialTheme.colors.error, fontWeight = FontWeight.Bold)) |
|
} |
|
} |
|
|
|
val lightThemeColors = lightColors( |
|
primary = Color(0xFFDD0D3C), |
|
primaryVariant = Color(0xFFC20029), |
|
secondary = Color.White, |
|
error = Color(0xFFD00036) |
|
) |
|
|
|
fun parseColor(hexString: String): Color { |
|
val red = parseInt(hexString.subSequence(0, 2).toString(), 16) |
|
val green = parseInt(hexString.subSequence(2, 4).toString(), 16) |
|
val blue = parseInt(hexString.subSequence(4, 6).toString(), 16) |
|
return Color(red, green, blue) |
|
}
|
|
|