package org.terst.feedreader.data import android.content.Context import org.terst.feedreader.readability.ReaderExtractor import org.terst.feedreader.stash.StashClient import org.terst.feedreader.stash.StashSaveResult import java.security.MessageDigest fun urlToArticleId(url: String): String = MessageDigest.getInstance("SHA-256").digest(url.toByteArray()).joinToString("") { "%02x".format(it) } /** * Creates (or returns the existing) standalone ArticleEntity for a bare URL not tied to any * feed, extracting it fresh via Readability if it's not already on-device. Shared by every * "save something without opening it in the reader first" entry point: the share-sheet * target, and long-pressing a link inside an article to save/stash it directly. */ suspend fun ensureStandaloneArticle(context: Context, db: AppDatabase, url: String): ArticleEntity { val id = urlToArticleId(url) db.articleDao().getById(id)?.let { return it } val extracted = ReaderExtractor.extract(context, url) val entity = ArticleEntity( id = id, feedId = null, title = extracted?.title ?: url, link = url, extractedContent = extracted?.content, createdAt = System.currentTimeMillis() ) db.articleDao().upsert(entity) return entity } /** * Outcome of a Stash save, already carrying the exact snackbar text and (when the save * succeeded, in either sense) the artifact id -- the same three-way handling (persist the * id, pick a message) was independently copy-pasted at 5 call sites (ShareReceiverActivity, * ReaderScreen x2, LinkReaderScreen x2) before this, all doing the identical * request-build-and-branch dance. Most callers only need [message] (and, to also update * their own local Compose state like `article = a.copy(stashArtifactId = ...)`, * [artifactId]) -- it's a sealed class rather than a flat data class so a caller that DOES * care about the Saved/AlreadySaved distinction (ShareReceiverActivity's wording does) can * still pattern-match exhaustively instead of parsing [message] back apart. */ sealed class StashSaveOutcome(val message: String, val artifactId: String?) { class Saved(artifactId: String) : StashSaveOutcome("Saved to Stash", artifactId) class AlreadySaved(artifactId: String) : StashSaveOutcome("Already in Stash", artifactId) class Failed(val reason: String) : StashSaveOutcome("Save failed: $reason", null) } suspend fun saveArticleToStash(context: Context, db: AppDatabase, articleId: String, url: String, title: String?): StashSaveOutcome { val settings = Settings(context) val client = StashClient(Net.client, settings.stashBaseUrlNow(), settings.stashApiKeyNow()) return when (val result = client.saveArticle(url, title)) { is StashSaveResult.Saved -> { db.articleDao().setStashArtifactId(articleId, result.artifact.id) StashSaveOutcome.Saved(result.artifact.id) } is StashSaveResult.AlreadySaved -> { db.articleDao().setStashArtifactId(articleId, result.existingId) StashSaveOutcome.AlreadySaved(result.existingId) } is StashSaveResult.Failed -> StashSaveOutcome.Failed(result.message) } }