neiam
/feedpug
feedpub
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
Android: string (UUID) ids + bidirectional swipe
7a29e6f · gmorell · 2026-06-25 18:00
Message
{commit_body(@commit)}
Files changed
modified
android/app/src/main/java/org/neiam/feedpug/app/MainActivity.kt
+2
−2
@@ -99,9 +99,9 @@ fun FeedPugApp(
| 99 | 99 | } |
| 100 | 100 | composable( |
| 101 | 101 | "detail/{id}", |
| 102 | − arguments = listOf(navArgument("id") { type = NavType.LongType }), | |
| 102 | + arguments = listOf(navArgument("id") { type = NavType.StringType }), | |
| 103 | 103 | ) { backStack -> |
| 104 | − val id = backStack.arguments?.getLong("id") ?: 0L | |
| 104 | + val id = backStack.arguments?.getString("id") ?: "" | |
| 105 | 105 | DetailScreen(tokens = tokens, itemId = id, onBack = { nav.popBackStack() }) |
| 106 | 106 | } |
| 107 | 107 | } |
modified
android/app/src/main/java/org/neiam/feedpug/app/data/FeedPugClient.kt
+4
−4
@@ -31,16 +31,16 @@ interface FeedPugApi {
| 31 | 31 | suspend fun readAll(@Query("sources") sources: String? = null): OkResponse |
| 32 | 32 | |
| 33 | 33 | @GET("api/items/{id}") |
| 34 | − suspend fun item(@Path("id") id: Long): ItemResponse | |
| 34 | + suspend fun item(@Path("id") id: String): ItemResponse | |
| 35 | 35 | |
| 36 | 36 | @POST("api/items/{id}/read") |
| 37 | − suspend fun markRead(@Path("id") id: Long): OkResponse | |
| 37 | + suspend fun markRead(@Path("id") id: String): OkResponse | |
| 38 | 38 | |
| 39 | 39 | @POST("api/items/{id}/unread") |
| 40 | − suspend fun markUnread(@Path("id") id: Long): OkResponse | |
| 40 | + suspend fun markUnread(@Path("id") id: String): OkResponse | |
| 41 | 41 | |
| 42 | 42 | @POST("api/items/{id}/reactions") |
| 43 | − suspend fun react(@Path("id") id: Long, @Body body: ReactBody): ReactResponse | |
| 43 | + suspend fun react(@Path("id") id: String, @Body body: ReactBody): ReactResponse | |
| 44 | 44 | |
| 45 | 45 | @GET("api/sources") |
| 46 | 46 | suspend fun sources(): SourcesResponse |
modified
android/app/src/main/java/org/neiam/feedpug/app/data/Models.kt
+5
−5
@@ -7,11 +7,11 @@ import kotlinx.serialization.Serializable
| 7 | 7 | data class ProfileResponse(val user: User) |
| 8 | 8 | |
| 9 | 9 | @Serializable |
| 10 | −data class User(val id: Long, val email: String) | |
| 10 | +data class User(val id: String, val email: String) | |
| 11 | 11 | |
| 12 | 12 | @Serializable |
| 13 | 13 | data class Feed( |
| 14 | − val id: Long, | |
| 14 | + val id: String, | |
| 15 | 15 | val title: String? = null, |
| 16 | 16 | val url: String? = null, |
| 17 | 17 | @SerialName("site_url") val siteUrl: String? = null, |
@@ -20,7 +20,7 @@ data class Feed(
| 20 | 20 | |
| 21 | 21 | @Serializable |
| 22 | 22 | data class Item( |
| 23 | − val id: Long, | |
| 23 | + val id: String, | |
| 24 | 24 | val title: String? = null, |
| 25 | 25 | val url: String? = null, |
| 26 | 26 | val summary: String? = null, |
@@ -55,7 +55,7 @@ data class SourcesResponse(val sources: List<Source>)
| 55 | 55 | |
| 56 | 56 | @Serializable |
| 57 | 57 | data class Reaction( |
| 58 | − val id: Long, | |
| 58 | + val id: String, | |
| 59 | 59 | val emoji: String, |
| 60 | 60 | val label: String? = null, |
| 61 | 61 | val position: Int = 0, |
@@ -66,7 +66,7 @@ data class ReactionsResponse(val reactions: List<Reaction>)
| 66 | 66 | |
| 67 | 67 | @Serializable |
| 68 | 68 | data class Slice( |
| 69 | − val id: Long, | |
| 69 | + val id: String, | |
| 70 | 70 | val name: String, |
| 71 | 71 | @SerialName("source_keys") val sourceKeys: List<String> = emptyList(), |
| 72 | 72 | @SerialName("unread_only") val unreadOnly: Boolean = false, |
modified
android/app/src/main/java/org/neiam/feedpug/app/ui/DetailScreen.kt
+1
−1
@@ -28,7 +28,7 @@ import org.neiam.feedpug.app.data.TokenStore
| 28 | 28 | @Composable |
| 29 | 29 | fun DetailScreen( |
| 30 | 30 | tokens: TokenStore, |
| 31 | − itemId: Long, | |
| 31 | + itemId: String, | |
| 32 | 32 | onBack: () -> Unit, |
| 33 | 33 | ) { |
| 34 | 34 | val context = LocalContext.current |
modified
android/app/src/main/java/org/neiam/feedpug/app/ui/TimelineScreen.kt
+38
−11
@@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.items
| 9 | 9 | import androidx.compose.material.icons.Icons |
| 10 | 10 | import androidx.compose.material.icons.automirrored.filled.Logout |
| 11 | 11 | import androidx.compose.material.icons.filled.DoneAll |
| 12 | +import androidx.compose.material.icons.filled.MarkEmailRead | |
| 12 | 13 | import androidx.compose.material.icons.filled.MarkEmailUnread |
| 13 | 14 | import androidx.compose.material.icons.filled.Palette |
| 14 | 15 | import androidx.compose.material.icons.filled.Refresh |
@@ -33,7 +34,7 @@ fun TimelineScreen(
| 33 | 34 | tokens: TokenStore, |
| 34 | 35 | currentTheme: AppTheme, |
| 35 | 36 | onPickTheme: (AppTheme) -> Unit, |
| 36 | − onOpenItem: (Long) -> Unit, | |
| 37 | + onOpenItem: (String) -> Unit, | |
| 37 | 38 | onUnpair: () -> Unit, |
| 38 | 39 | ) { |
| 39 | 40 | val creds = remember { tokens.load() } |
@@ -46,7 +47,7 @@ fun TimelineScreen(
| 46 | 47 | var query by remember { mutableStateOf("") } |
| 47 | 48 | var sourcesCsv by remember { mutableStateOf<String?>(null) } |
| 48 | 49 | var reactionEmoji by remember { mutableStateOf<String?>(null) } |
| 49 | − var activeSliceId by remember { mutableStateOf<Long?>(null) } | |
| 50 | + var activeSliceId by remember { mutableStateOf<String?>(null) } | |
| 50 | 51 | var slices by remember { mutableStateOf<List<Slice>>(emptyList()) } |
| 51 | 52 | var loading by remember { mutableStateOf(false) } |
| 52 | 53 | var error by remember { mutableStateOf<String?>(null) } |
@@ -73,6 +74,13 @@ fun TimelineScreen(
| 73 | 74 | } |
| 74 | 75 | } |
| 75 | 76 | |
| 77 | + fun setRead(item: Item) { | |
| 78 | + scope.launch { | |
| 79 | + api?.runCatching { markRead(item.id) } | |
| 80 | + items = items.map { if (it.id == item.id) it.copy(read = true) else it } | |
| 81 | + } | |
| 82 | + } | |
| 83 | + | |
| 76 | 84 | fun setUnread(item: Item) { |
| 77 | 85 | scope.launch { |
| 78 | 86 | api?.runCatching { markUnread(item.id) } |
@@ -185,6 +193,7 @@ fun TimelineScreen(
| 185 | 193 | SwipeableTimelineRow( |
| 186 | 194 | item = item, |
| 187 | 195 | onOpen = { onOpenItem(item.id) }, |
| 196 | + onMarkRead = { setRead(item) }, | |
| 188 | 197 | onMarkUnread = { setUnread(item) }, |
| 189 | 198 | ) |
| 190 | 199 | HorizontalDivider() |
@@ -210,31 +219,49 @@ fun TimelineScreen(
| 210 | 219 | |
| 211 | 220 | @OptIn(ExperimentalMaterial3Api::class) |
| 212 | 221 | @Composable |
| 213 | −private fun SwipeableTimelineRow(item: Item, onOpen: () -> Unit, onMarkUnread: () -> Unit) { | |
| 222 | +private fun SwipeableTimelineRow( | |
| 223 | + item: Item, | |
| 224 | + onOpen: () -> Unit, | |
| 225 | + onMarkRead: () -> Unit, | |
| 226 | + onMarkUnread: () -> Unit, | |
| 227 | +) { | |
| 214 | 228 | val dismissState = rememberSwipeToDismissBoxState( |
| 215 | 229 | confirmValueChange = { value -> |
| 216 | − // Swipe-left triggers the action but the row never leaves the list. | |
| 217 | − if (value == SwipeToDismissBoxValue.EndToStart) onMarkUnread() | |
| 230 | + // Either direction triggers an action; the row never leaves the list. | |
| 231 | + when (value) { | |
| 232 | + SwipeToDismissBoxValue.StartToEnd -> onMarkRead() | |
| 233 | + SwipeToDismissBoxValue.EndToStart -> onMarkUnread() | |
| 234 | + SwipeToDismissBoxValue.Settled -> Unit | |
| 235 | + } | |
| 218 | 236 | false |
| 219 | 237 | }, |
| 220 | 238 | ) |
| 221 | 239 | |
| 222 | 240 | SwipeToDismissBox( |
| 223 | 241 | state = dismissState, |
| 224 | − enableDismissFromStartToEnd = false, | |
| 225 | − enableDismissFromEndToStart = true, | |
| 226 | 242 | backgroundContent = { |
| 243 | + // Swipe right → mark read; swipe left → mark unread. | |
| 244 | + val toRead = dismissState.dismissDirection == SwipeToDismissBoxValue.StartToEnd | |
| 227 | 245 | Row( |
| 228 | 246 | Modifier |
| 229 | 247 | .fillMaxSize() |
| 230 | − .background(MaterialTheme.colorScheme.primaryContainer) | |
| 248 | + .background( | |
| 249 | + if (toRead) MaterialTheme.colorScheme.secondaryContainer | |
| 250 | + else MaterialTheme.colorScheme.primaryContainer | |
| 251 | + ) | |
| 231 | 252 | .padding(horizontal = 20.dp), |
| 232 | 253 | verticalAlignment = Alignment.CenterVertically, |
| 233 | − horizontalArrangement = Arrangement.End, | |
| 254 | + horizontalArrangement = if (toRead) Arrangement.Start else Arrangement.End, | |
| 234 | 255 | ) { |
| 235 | − Icon(Icons.Default.MarkEmailUnread, contentDescription = null) | |
| 256 | + Icon( | |
| 257 | + if (toRead) Icons.Default.MarkEmailRead else Icons.Default.MarkEmailUnread, | |
| 258 | + contentDescription = null, | |
| 259 | + ) | |
| 236 | 260 | Spacer(Modifier.width(8.dp)) |
| 237 | − Text("Mark unread", style = MaterialTheme.typography.labelLarge) | |
| 261 | + Text( | |
| 262 | + if (toRead) "Mark read" else "Mark unread", | |
| 263 | + style = MaterialTheme.typography.labelLarge, | |
| 264 | + ) | |
| 238 | 265 | } |
| 239 | 266 | }, |
| 240 | 267 | content = { |
Parents: 0bf367a