@@ -3,12 +3,17 @@ package site.remlit.snowdrop.view
33import androidx.compose.foundation.clickable
44import androidx.compose.foundation.interaction.MutableInteractionSource
55import androidx.compose.foundation.layout.Arrangement
6+ import androidx.compose.foundation.layout.Box
67import androidx.compose.foundation.layout.Column
8+ import androidx.compose.foundation.layout.Row
79import androidx.compose.foundation.layout.fillMaxSize
10+ import androidx.compose.foundation.layout.padding
811import androidx.compose.foundation.lazy.LazyColumn
912import androidx.compose.foundation.lazy.LazyListState
1013import androidx.compose.foundation.lazy.items
1114import androidx.compose.foundation.lazy.rememberLazyListState
15+ import androidx.compose.material3.DropdownMenu
16+ import androidx.compose.material3.DropdownMenuItem
1217import androidx.compose.material3.Icon
1318import androidx.compose.material3.IconButton
1419import androidx.compose.material3.Text
@@ -17,36 +22,59 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
1722import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
1823import androidx.compose.runtime.Composable
1924import androidx.compose.runtime.LaunchedEffect
25+ import androidx.compose.runtime.getValue
2026import androidx.compose.runtime.mutableStateListOf
2127import androidx.compose.runtime.mutableStateOf
2228import androidx.compose.runtime.remember
2329import androidx.compose.runtime.rememberCoroutineScope
30+ import androidx.compose.runtime.setValue
2431import androidx.compose.runtime.snapshotFlow
2532import androidx.compose.ui.Alignment
2633import androidx.compose.ui.Modifier
2734import androidx.compose.ui.geometry.Offset
2835import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
2936import androidx.compose.ui.input.nestedscroll.NestedScrollSource
3037import androidx.compose.ui.input.nestedscroll.nestedScroll
38+ import androidx.compose.ui.unit.dp
39+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
40+ import com.russhwolf.settings.ExperimentalSettingsApi
3141import kotlinx.coroutines.flow.collect
3242import kotlinx.coroutines.flow.distinctUntilChanged
3343import kotlinx.coroutines.flow.filter
3444import kotlinx.coroutines.flow.map
3545import kotlinx.coroutines.flow.onEach
3646import kotlinx.coroutines.launch
47+ import kotlinx.coroutines.runBlocking
3748import org.jetbrains.compose.resources.painterResource
3849import org.jetbrains.compose.resources.stringResource
3950import site.remlit.snowdrop.SettingsRoute
4051import site.remlit.snowdrop.api.timeline.getHomeTimeline
52+ import site.remlit.snowdrop.api.timeline.getPublicTimeline
4153import site.remlit.snowdrop.component.Status
4254import site.remlit.snowdrop.component.ViewSurface
55+ import site.remlit.snowdrop.model.ApiResponse
4356import site.remlit.snowdrop.model.Status
4457import site.remlit.snowdrop.util.LocalNavController
58+ import site.remlit.snowdrop.util.bg
59+ import site.remlit.snowdrop.util.blockingSettings
60+ import site.remlit.snowdrop.util.getFeature
4561import site.remlit.snowdrop.util.scrollingUpward
62+ import site.remlit.snowdrop.util.settings
4663import snowdrop.shared.generated.resources.Res
4764import snowdrop.shared.generated.resources.add_account
65+ import snowdrop.shared.generated.resources.bubble
66+ import snowdrop.shared.generated.resources.global
67+ import snowdrop.shared.generated.resources.home
68+ import snowdrop.shared.generated.resources.icon_bubble_chart_24px
69+ import snowdrop.shared.generated.resources.icon_globe_24px
70+ import snowdrop.shared.generated.resources.icon_home_24px
71+ import snowdrop.shared.generated.resources.icon_keyboard_arrow_down_24px
72+ import snowdrop.shared.generated.resources.icon_keyboard_arrow_up_24px
73+ import snowdrop.shared.generated.resources.icon_map_24px
4874import snowdrop.shared.generated.resources.icon_settings_24px
75+ import snowdrop.shared.generated.resources.local
4976import snowdrop.shared.generated.resources.timeline
77+ import kotlin.math.sin
5078
5179@Composable
5280inline fun LazyListState.ScrollEndCallback (crossinline callback : () -> Unit ) {
@@ -63,6 +91,7 @@ inline fun LazyListState.ScrollEndCallback(crossinline callback: () -> Unit) {
6391 }
6492}
6593
94+ @OptIn(ExperimentalSettingsApi ::class )
6695@Composable
6796fun TimelineView () = ViewSurface {
6897 Column (
@@ -72,47 +101,131 @@ fun TimelineView() = ViewSurface {
72101 val navHandler = LocalNavController .current
73102 val coroutineScope = rememberCoroutineScope()
74103
104+ // 0 - home, 1 - local, 2 - bubble, 3 - global
105+ val timelineType by settings.getIntFlow(" timeline" , 0 )
106+ .collectAsStateWithLifecycle(0 )
107+ var timelinePickerOpen by remember { mutableStateOf(false ) }
108+
109+ suspend fun getTimeline (
110+ maxId : String? = null,
111+ minId : String? = null,
112+ sinceId : String? = null
113+ ): ApiResponse <List <Status >> {
114+ return when (timelineType) {
115+ 0 -> getHomeTimeline(maxId = maxId, minId = minId, sinceId = sinceId)
116+ 1 -> getPublicTimeline(maxId = maxId, minId = minId, sinceId = sinceId, local = true )
117+ 2 -> getPublicTimeline(maxId = maxId, minId = minId, sinceId = sinceId, remote = true ) // todo: fix bubble timeline
118+ else -> getPublicTimeline(maxId = maxId, minId = minId, sinceId = sinceId, remote = true ) // else also 3
119+ }
120+ }
121+
75122 val timeline = remember { mutableStateListOf<Status >() }
76123 val refreshState = rememberPullToRefreshState()
77124 val listState = rememberLazyListState().also {
78125 it.ScrollEndCallback {
79126 coroutineScope.launch {
80- val res = getHomeTimeline (maxId = timeline.last().id)
127+ val res = getTimeline (maxId = timeline.last().id)
81128 if (res.error) return @launch
82129 if (res.response == null ) return @launch
83130 timeline.addAll(res.response)
84131 }
85132 }
86133 }
87- val isRefreshing = remember { mutableStateOf(false ) }
134+ var isRefreshing by remember { mutableStateOf(false ) }
88135
89136 suspend fun addOrUpdateTimeline () {
90- isRefreshing.value = true
91- val res = getHomeTimeline ()
137+ isRefreshing = true
138+ val res = getTimeline ()
92139 if (res.error) return
93140 if (res.response == null ) return
94141 timeline.clear()
95142 timeline.addAll(res.response)
96143 listState.scrollToItem(0 )
97- isRefreshing.value = false
144+ isRefreshing = false
145+ }
146+
147+ // whenever the timelineType changes, this function runs.
148+ LaunchedEffect (timelineType) { addOrUpdateTimeline() }
149+
150+ @Composable
151+ fun RenderTimelineTypeIcon (type : Int? = null) {
152+ when (type ? : timelineType) {
153+ 0 -> Icon (painterResource(Res .drawable.icon_home_24px), null )
154+ 1 -> Icon (painterResource(Res .drawable.icon_map_24px), null )
155+ 2 -> Icon (painterResource(Res .drawable.icon_bubble_chart_24px), null )
156+ 3 -> Icon (painterResource(Res .drawable.icon_globe_24px), null )
157+ }
98158 }
99159
100- LaunchedEffect (Unit ) {
101- addOrUpdateTimeline()
160+ @Composable
161+ fun RenderTimelineSelectionDropdown () {
162+ DropdownMenu (
163+ expanded = timelinePickerOpen,
164+ onDismissRequest = { timelinePickerOpen = false },
165+ ) {
166+ DropdownMenuItem (
167+ leadingIcon = { RenderTimelineTypeIcon (0 ) },
168+ text = { Text (stringResource(Res .string.home)) },
169+ onClick = { blockingSettings.putInt(" timeline" , 0 ); timelinePickerOpen = false }
170+ )
171+ DropdownMenuItem (
172+ leadingIcon = { RenderTimelineTypeIcon (1 ) },
173+ text = { Text (stringResource(Res .string.local)) },
174+ onClick = { blockingSettings.putInt(" timeline" , 1 ); timelinePickerOpen = false }
175+ )
176+ if (getFeature(" bubble_timeline" ))
177+ DropdownMenuItem (
178+ leadingIcon = { RenderTimelineTypeIcon (2 ) },
179+ text = { Text (stringResource(Res .string.bubble)) },
180+ onClick = { blockingSettings.putInt(" timeline" , 2 ); timelinePickerOpen = false }
181+ )
182+ DropdownMenuItem (
183+ leadingIcon = { RenderTimelineTypeIcon (3 ) },
184+ text = { Text (stringResource(Res .string.global)) },
185+ onClick = { blockingSettings.putInt(" timeline" , 3 ); timelinePickerOpen = false }
186+ )
187+ }
102188 }
103189
190+
191+ /*
192+ *
193+ * Actual Timeline View
194+ *
195+ */
104196 TopAppBar (
105197 modifier = Modifier .clickable(
106198 interactionSource = MutableInteractionSource (),
107199 indication = null ,
108200 onClick = {
201+ /*
109202 coroutineScope.launch {
110203 listState.animateScrollToItem(0)
111204 }
205+ */
206+
207+ timelinePickerOpen = ! timelinePickerOpen
112208 }
113209 ),
114210 title = {
115- Text (stringResource(Res .string.timeline))
211+ Row (
212+ horizontalArrangement = Arrangement .spacedBy(10 .dp),
213+ verticalAlignment = Alignment .CenterVertically
214+ ) {
215+ RenderTimelineTypeIcon ()
216+
217+ when (timelineType) {
218+ 0 -> Text (stringResource(Res .string.home))
219+ 1 -> Text (stringResource(Res .string.local))
220+ 2 -> Text (stringResource(Res .string.bubble))
221+ 3 -> Text (stringResource(Res .string.global))
222+ }
223+
224+ if (timelinePickerOpen) Icon (painterResource(Res .drawable.icon_keyboard_arrow_up_24px), null )
225+ else Icon (painterResource(Res .drawable.icon_keyboard_arrow_down_24px), null )
226+
227+ RenderTimelineSelectionDropdown ()
228+ }
116229 },
117230 actions = {
118231 IconButton (onClick = { navHandler.navigate(SettingsRoute ) }) {
@@ -122,7 +235,7 @@ fun TimelineView() = ViewSurface {
122235 )
123236
124237 PullToRefreshBox (
125- isRefreshing = isRefreshing.value ,
238+ isRefreshing = isRefreshing,
126239 state = refreshState,
127240 onRefresh = {
128241 coroutineScope.launch {
@@ -131,6 +244,7 @@ fun TimelineView() = ViewSurface {
131244 }
132245 }
133246 ) {
247+ // for determining if the compose FAB should be visible
134248 val nestedScrollConnection = remember {
135249 object : NestedScrollConnection {
136250 override fun onPreScroll (available : Offset , source : NestedScrollSource ): Offset {
@@ -144,18 +258,12 @@ fun TimelineView() = ViewSurface {
144258
145259 LazyColumn (
146260 state = listState,
147- modifier = Modifier
148- .fillMaxSize()
149- .nestedScroll(nestedScrollConnection),
261+ modifier = Modifier .fillMaxSize().nestedScroll(nestedScrollConnection),
150262 ) {
151263 items(
152264 items = timeline,
153- key = { status ->
154- status.id!!
155- }
156- ) { status ->
157- Status (status)
158- }
265+ key = { it.id }
266+ ) { Status (it) }
159267 }
160268 }
161269 }
0 commit comments