OXYGENHUB
# dfbc3e972872 oxygenhub-android / app / src / main / java / io / oxygenhub / android / ui / common / States.kt
3.7 KiB · last changed Sign-in, dashboard, repositories, issues and inbox screens, adaptive (refs #2) 11 days ago source blame history raw
1
package io.oxygenhub.android.ui.common
2
3
import android.text.format.DateUtils
4
import androidx.compose.foundation.layout.Arrangement
5
import androidx.compose.foundation.layout.Box
6
import androidx.compose.foundation.layout.Column
7
import androidx.compose.foundation.layout.Spacer
8
import androidx.compose.foundation.layout.fillMaxSize
9
import androidx.compose.foundation.layout.height
10
import androidx.compose.foundation.layout.padding
11
import androidx.compose.foundation.layout.size
12
import androidx.compose.material.icons.Icons
13
import androidx.compose.material.icons.outlined.CloudOff
14
import androidx.compose.material3.Button
15
import androidx.compose.material3.CircularProgressIndicator
16
import androidx.compose.material3.ExperimentalMaterial3Api
17
import androidx.compose.material3.Icon
18
import androidx.compose.material3.MaterialTheme
19
import androidx.compose.material3.Text
20
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
21
import androidx.compose.runtime.Composable
22
import androidx.compose.ui.Alignment
23
import androidx.compose.ui.Modifier
24
import androidx.compose.ui.graphics.vector.ImageVector
25
import androidx.compose.ui.text.style.TextAlign
26
import androidx.compose.ui.unit.dp
27
28
/** A spinner in the middle of the space. */
29
@Composable
30
fun LoadingState(modifier: Modifier = Modifier) {
31
    Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
32
        CircularProgressIndicator()
33
    }
34
}
35
36
/** Nothing to show, said plainly, with an icon. */
37
@Composable
38
fun EmptyState(
39
    icon: ImageVector,
40
    title: String,
41
    body: String,
42
    modifier: Modifier = Modifier,
43
    action: (@Composable () -> Unit)? = null,
44
) {
45
    Message(icon, title, body, modifier) {
46
        if (action != null) {
47
            Spacer(Modifier.height(8.dp))
48
            action()
49
        }
50
    }
51
}
52
53
/** What went wrong, and a way to try again. */
54
@Composable
55
fun ErrorState(message: String, onRetry: () -> Unit, modifier: Modifier = Modifier) {
56
    Message(Icons.Outlined.CloudOff, "Couldn't load this", message, modifier) {
57
        Spacer(Modifier.height(16.dp))
58
        Button(onClick = onRetry) { Text("Try again") }
59
    }
60
}
61
62
@Composable
63
private fun Message(
64
    icon: ImageVector,
65
    title: String,
66
    body: String,
67
    modifier: Modifier = Modifier,
68
    extra: @Composable () -> Unit = {},
69
) {
70
    Column(
71
        modifier.fillMaxSize().padding(32.dp),
72
        horizontalAlignment = Alignment.CenterHorizontally,
73
        verticalArrangement = Arrangement.Center,
74
    ) {
75
        Icon(icon, contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
76
        Spacer(Modifier.height(16.dp))
77
        Text(title, style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center)
78
        Spacer(Modifier.height(8.dp))
79
        Text(body, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
80
        extra()
81
    }
82
}
83
84
/**
85
 * The usual body of a screen over a [Load]: a spinner, the error with a
86
 * retry, or [content] in a pull-to-refresh box.
87
 */
88
@OptIn(ExperimentalMaterial3Api::class)
89
@Composable
90
fun <T> LoadContent(
91
    load: Load<T>,
92
    onRefresh: () -> Unit,
93
    modifier: Modifier = Modifier,
94
    content: @Composable (T) -> Unit,
95
) {
96
    when (load) {
97
        Load.Loading -> LoadingState(modifier)
98
        is Load.Failed -> ErrorState(load.message, onRefresh, modifier)
99
        is Load.Ready -> PullToRefreshBox(isRefreshing = load.refreshing, onRefresh = onRefresh, modifier = modifier.fillMaxSize()) {
100
            content(load.value)
101
        }
102
    }
103
}
104
105
/** "3 hours ago", from milliseconds since the epoch. */
106
fun relativeTime(ms: Long, now: Long = System.currentTimeMillis()): String =
107
    if (ms <= 0) "" else DateUtils.getRelativeTimeSpanString(ms, now, DateUtils.MINUTE_IN_MILLIS).toString()