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.StashSaveOutcome import org.terst.feedreader.data.ensureStandaloneArticle import org.terst.feedreader.data.saveArticleToStash 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 null) } var done by remember { mutableStateOf(url == null) } // Requires an explicit tap before touching the network -- this activity is exported // (any installed app can target it directly with an ACTION_SEND intent, not just via // the system share sheet), and starting extraction/Stash-save immediately on launch // meant ANY other app could silently make this one fetch+JS-render an arbitrary URL // and archive it to the user's own Stash server with zero user interaction. A tap // here is the only thing standing between "another app sent an intent" and "this app // did real network work," so it can't be skipped just because url is non-null. var confirmed by remember { mutableStateOf(false) } LaunchedEffect(url, confirmed) { if (url == null || !confirmed) return@LaunchedEffect status = "Saving…" val db = AppDatabase.get(context) val entity = ensureStandaloneArticle(context, db, url) if (!entity.isSaved) db.articleDao().setSaved(entity.id, true) status = when (val outcome = saveArticleToStash(context, db, entity.id, url, entity.title)) { is StashSaveOutcome.Saved -> "Saved for offline reading and archived to Stash" is StashSaveOutcome.AlreadySaved -> "Saved for offline reading (already in Stash)" is StashSaveOutcome.Failed -> "Saved for offline reading. Stash archive failed: ${outcome.reason}" } 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)) when { url == null -> { Text(status ?: "No link found in shared content") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = onDone) { Text("Done") } } !confirmed -> { Text(url) Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { confirmed = true }) { Text("Save") } } else -> { Text(status ?: "Saving…") if (!done) { Spacer(modifier = Modifier.height(8.dp)) CircularProgressIndicator() } else { Spacer(modifier = Modifier.height(8.dp)) Button(onClick = onDone) { Text("Done") } } } } } } } }