diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-22 08:08:58 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-22 08:08:58 +0000 |
| commit | d8ce4b3c0f383e844b125d2d3abd618715bb3c89 (patch) | |
| tree | b586ff9b42392f2c44645773c2a2f2aadb9db406 /app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt | |
Initial commit: Feeds, an Android RSS reader
Feed subscriptions (RSS 2.0 + Atom, hand-rolled parser), OPML
import/export, WorkManager periodic fetch with conditional GET,
Room-backed local storage (feeds/articles/filter rules), keyword
mute/highlight filtering.
Reader mode: on-device extraction via Mozilla's Readability.js
(vendored from github.com/mozilla/readability, pinned to release
0.6.0) run in a hidden WebView, for feed articles whose provided
content is too thin to be the real article body.
Save: explicit action (from a feed article, or the Android share-sheet
target for any URL) archives to Stash via POST /artifacts?sync=true,
while a local on-device copy (Readability-extracted) is always kept so
saved articles read offline regardless of Stash's reachability.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt | 113 |
1 files changed, 113 insertions, 0 deletions
diff --git a/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt b/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt new file mode 100644 index 0000000..656bebc --- /dev/null +++ b/app/src/main/java/org/terst/feedreader/ui/ShareReceiverActivity.kt @@ -0,0 +1,113 @@ +package org.terst.feedreader.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import org.terst.feedreader.data.AppDatabase +import org.terst.feedreader.data.ArticleEntity +import org.terst.feedreader.data.Net +import org.terst.feedreader.data.Settings +import org.terst.feedreader.readability.ReaderExtractor +import org.terst.feedreader.stash.StashClient +import org.terst.feedreader.stash.StashSaveResult +import java.security.MessageDigest + +private val URL_REGEX = Regex("""https?://\S+""") + +/** + * Share-sheet target: a URL shared from any other app lands here. Extracts + * the article on-device (so it's readable offline immediately, independent + * of Stash), stores it locally as a standalone saved article (feedId=null, + * not tied to any subscription), and best-effort archives it to Stash. + */ +class ShareReceiverActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val sharedText = intent?.getStringExtra(android.content.Intent.EXTRA_TEXT).orEmpty() + val url = URL_REGEX.find(sharedText)?.value + + setContent { + MaterialTheme { + ShareReceiverContent(url = url, onDone = { finish() }) + } + } + } +} + +@Composable +private fun ShareReceiverContent(url: String?, onDone: () -> Unit) { + val context = LocalContext.current + var status by remember { mutableStateOf(if (url == null) "No link found in shared content" else "Saving…") } + var done by remember { mutableStateOf(url == null) } + + LaunchedEffect(url) { + if (url == null) return@LaunchedEffect + val db = AppDatabase.get(context) + + val extracted = ReaderExtractor.extract(context, url) + val id = MessageDigest.getInstance("SHA-256").digest(url.toByteArray()).joinToString("") { "%02x".format(it) } + db.articleDao().upsert( + ArticleEntity( + id = id, + feedId = null, + title = extracted?.title ?: url, + link = url, + extractedContent = extracted?.content, + isSaved = true, + createdAt = System.currentTimeMillis() + ) + ) + + val settings = Settings(context) + val client = StashClient(Net.client, settings.stashBaseUrlNow(), settings.stashApiKeyNow()) + status = when (val result = client.saveArticle(url, extracted?.title)) { + is StashSaveResult.Saved -> { + db.articleDao().setStashArtifactId(id, result.artifact.id) + "Saved for offline reading and archived to Stash" + } + is StashSaveResult.AlreadySaved -> { + db.articleDao().setStashArtifactId(id, result.existingId) + "Saved for offline reading (already in Stash)" + } + is StashSaveResult.Failed -> + "Saved for offline reading. Stash archive failed: ${result.message}" + } + done = true + } + + Column(modifier = Modifier.padding(24.dp)) { + Card(modifier = Modifier.padding(8.dp)) { + Column(modifier = Modifier.padding(20.dp)) { + Text("Feeds", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + Text(status) + if (!done) { + Spacer(modifier = Modifier.height(8.dp)) + CircularProgressIndicator() + } else { + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = onDone) { Text("Done") } + } + } + } + } +} |
