1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
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)
}
}
|