From f202c603259c40c4d21a4385c9f69a3efca87acc Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sat, 25 Jul 2026 00:55:59 +0000 Subject: Text-only bottom bar, re-extract action, fix YouTube thumbnails, verify Stash creds before saving - Bottom bar: Previous/Keep unread/Next are now plain text labels, no icons - Add "Re-download and re-extract" to the overflow menu (ReaderScreen and LinkReaderScreen) to force a fresh fetch + Readability pass on demand, overwriting whatever's cached - Fix a real compile bug in the previous commit's reader-mode-off handler (passed null into a non-null setExtractedContent) via a proper clearExtractedContent() DAO query - YouTube embeds: fixed the broken-image-glyph bug two ways -- (1) an 11-char video-ID regex was accidentally matching a playlist embed's literal "videoseries" placeholder path, building a thumbnail URL for a video that doesn't exist, and (2) Readability's link-density cleanup was discarding the replacement entirely (a bare reads as an ad/share unit). Restructured as a
/
pair -- the same shape a normal captioned photo takes -- confirmed live this survives extraction intact - Stash settings: API key field is now a password field with a show/hide toggle, and "Verify and save" makes a real authenticated request before persisting anything, showing why it failed instead of silently saving bad credentials Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL --- .../org/terst/feedreader/ui/LinkReaderScreen.kt | 24 ++++++++++ .../java/org/terst/feedreader/ui/ReaderScreen.kt | 49 ++++++++++++-------- .../java/org/terst/feedreader/ui/ReaderWebView.kt | 6 --- .../java/org/terst/feedreader/ui/SettingsScreen.kt | 54 ++++++++++++++++++++-- 4 files changed, 106 insertions(+), 27 deletions(-) (limited to 'app/src/main/java/org/terst/feedreader/ui') diff --git a/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt index c001b69..c29a585 100644 --- a/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt @@ -80,6 +80,7 @@ fun LinkReaderScreen(url: String, onBack: () -> Unit, onOpenLink: (String) -> Un var extractionFailed by remember(url) { mutableStateOf(false) } var isReaderMode by remember(url) { mutableStateOf(true) } var isSaving by remember(url) { mutableStateOf(false) } + var isReextracting by remember(url) { mutableStateOf(false) } var moreMenuExpanded by remember(url) { mutableStateOf(false) } var longPressedLink by remember(url) { mutableStateOf(null) } @@ -193,6 +194,29 @@ fun LinkReaderScreen(url: String, onBack: () -> Unit, onOpenLink: (String) -> Un Icon(Icons.Default.MoreVert, contentDescription = "More") } DropdownMenu(expanded = moreMenuExpanded, onDismissRequest = { moreMenuExpanded = false }) { + DropdownMenuItem( + enabled = !isReextracting, + text = { Text("Re-download and re-extract") }, + onClick = { + moreMenuExpanded = false + scope.launch { + isReextracting = true + val extracted = ReaderExtractor.extract(context, url) + isReextracting = false + val content = extracted?.content + if (content != null) { + extractedTitle = extracted.title + extractedContent = content + extractionFailed = false + isReaderMode = true + db.articleDao().setExtractedContent(articleId, content) + snackbarHostState.showSnackbar("Re-extracted") + } else { + snackbarHostState.showSnackbar("Re-extraction failed") + } + } + } + ) DropdownMenuItem( text = { Text("Find another source") }, onClick = { diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt index b7f6018..06227b7 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt @@ -24,10 +24,7 @@ import androidx.compose.material.icons.filled.BookmarkBorder import androidx.compose.material.icons.filled.CloudDone import androidx.compose.material.icons.filled.CloudUpload import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.NavigateBefore -import androidx.compose.material.icons.filled.NavigateNext import androidx.compose.material.icons.filled.OpenInBrowser -import androidx.compose.material.icons.filled.SkipNext import androidx.compose.material.icons.outlined.Article import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomAppBar @@ -252,7 +249,7 @@ private fun ReaderContent( } else { isReaderMode = false readerContent = null - db.articleDao().setExtractedContent(articleId, null) + db.articleDao().clearExtractedContent(articleId) snackbarHostState.showSnackbar("Reader mode disabled for $feedLabel") } } @@ -393,6 +390,32 @@ private fun ReaderContent( onOpenBrowser(a.link) } ) + // Forces a fresh fetch + Readability pass, overwriting whatever's + // cached -- for when extraction came back thin, stale, or broken + // (e.g. a YouTube thumbnail that failed to resolve) and simply hasn't + // been retried since whatever caused that. + DropdownMenuItem( + enabled = !isExtracting, + text = { Text("Re-download and re-extract") }, + onClick = { + moreMenuExpanded = false + val a = currentArticle ?: return@DropdownMenuItem + scope.launch { + isExtracting = true + val extracted = ReaderExtractor.extract(context, a.link) + isExtracting = false + val content = extracted?.content + if (content != null) { + readerContent = content + isReaderMode = true + db.articleDao().setExtractedContent(a.id, content) + snackbarHostState.showSnackbar("Re-extracted") + } else { + snackbarHostState.showSnackbar("Re-extraction failed") + } + } + } + ) DropdownMenuItem( text = { Text("Report bug") }, onClick = { @@ -406,26 +429,16 @@ private fun ReaderContent( }, bottomBar = { BottomAppBar { - TextButton( - onClick = onPrevious, - enabled = hasPrevious, - modifier = Modifier.weight(1f) - ) { - Icon(Icons.Default.NavigateBefore, contentDescription = null) + TextButton(onClick = onPrevious, enabled = hasPrevious, modifier = Modifier.weight(1f)) { Text("Previous") } // Advances like Next, but skips the implicit "mark read" that moving on // normally carries -- for "I looked, but I'm not done with this one yet." - IconButton(onClick = onNextKeepUnread, enabled = hasNext) { - Icon(Icons.Default.SkipNext, contentDescription = "Next, keep unread") + TextButton(onClick = onNextKeepUnread, enabled = hasNext, modifier = Modifier.weight(1f)) { + Text("Keep unread") } - TextButton( - onClick = onNext, - enabled = hasNext, - modifier = Modifier.weight(1f) - ) { + TextButton(onClick = onNext, enabled = hasNext, modifier = Modifier.weight(1f)) { Text("Next") - Icon(Icons.Default.NavigateNext, contentDescription = null) } } }, diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt index b984d1d..1fd2411 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt @@ -135,12 +135,6 @@ fun wrapReaderHtml(bodyContent: String, title: String? = null): String = """ explicit width/height was deliberately sized by the page itself, so leave those alone -- just cap the ones that would otherwise size themselves. */ svg:not([width]):not([height]) { max-width: 32px; max-height: 32px; } - /* A tap-to-open thumbnail standing in for a YouTube embed (see ReaderExtractor) -- - JS is off here, so the real embedded player could never have worked anyway. */ - .yt-embed { position: relative; display: block; margin: 0.5em 0; } - .yt-embed .yt-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); - width: 0; height: 0; border-style: solid; border-width: 18px 0 18px 30px; - border-color: transparent transparent transparent rgba(255,255,255,0.85); } ${if (!title.isNullOrBlank()) "

${escapeHtml(title)}

" else ""}$bodyContent """.trimIndent() diff --git a/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt b/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt index b7897f4..3bd6e3e 100644 --- a/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/SettingsScreen.kt @@ -13,7 +13,10 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -36,12 +39,17 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.terst.feedreader.data.AppDatabase import org.terst.feedreader.data.FilterAction import org.terst.feedreader.data.FilterRuleEntity +import org.terst.feedreader.data.Net import org.terst.feedreader.data.Settings +import org.terst.feedreader.stash.StashClient +import org.terst.feedreader.stash.StashVerifyResult @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -53,6 +61,9 @@ fun SettingsScreen(onBack: () -> Unit) { var stashUrl by remember { mutableStateOf("") } var stashKey by remember { mutableStateOf("") } + var stashKeyVisible by remember { mutableStateOf(false) } + var isVerifyingStash by remember { mutableStateOf(false) } + var stashStatus by remember { mutableStateOf(null) } LaunchedEffect(Unit) { stashUrl = settings.stashBaseUrlNow() stashKey = settings.stashApiKeyNow() @@ -105,15 +116,52 @@ fun SettingsScreen(onBack: () -> Unit) { Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( value = stashKey, - onValueChange = { stashKey = it }, + onValueChange = { stashKey = it; stashStatus = null }, label = { Text("API key") }, + singleLine = true, + visualTransformation = if (stashKeyVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { stashKeyVisible = !stashKeyVisible }) { + Icon( + if (stashKeyVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility, + contentDescription = if (stashKeyVisible) "Hide API key" else "Show API key" + ) + } + }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) Button( - onClick = { scope.launch { settings.setStash(stashUrl, stashKey) } }, + enabled = !isVerifyingStash, + onClick = { + scope.launch { + isVerifyingStash = true + stashStatus = null + val client = StashClient(Net.client, stashUrl.trim(), stashKey.trim()) + when (val result = client.verifyCredentials()) { + is StashVerifyResult.Valid -> { + settings.setStash(stashUrl.trim(), stashKey.trim()) + stashStatus = "Verified and saved" + } + is StashVerifyResult.Invalid -> { + stashStatus = result.message + } + } + isVerifyingStash = false + } + }, modifier = Modifier.fillMaxWidth() - ) { Text("Save") } + ) { + if (isVerifyingStash) { + CircularProgressIndicator(modifier = Modifier.height(20.dp).width(20.dp)) + } else { + Text("Verify and save") + } + } + stashStatus?.let { + Spacer(modifier = Modifier.height(8.dp)) + Text(it, style = MaterialTheme.typography.bodySmall) + } } HorizontalDivider() } -- cgit v1.2.3