summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/data/ArticleSaving.kt
blob: 53d80eb6c9b213a02f3cd698d01971df8d848a31 (plain)
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)
    }
}