mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
feat: preview PDF links inline with first-page thumbnail and full pager
Adds MediaUrlPdf / PdfSegment to the rich-text pipeline so PDF URLs (detected by .pdf extension, NIP-92 imeta m tag, or application/pdf Content-Type) render a card showing the first page, filename, and page count. Tapping opens a full-screen HorizontalPager over every page rendered on demand with PdfRenderer. Long-press surfaces the existing share menu. Uses only the built-in Android PdfRenderer; no new dependencies. Desktop continues to fall back to a clickable link.
This commit is contained in:
+11
@@ -68,6 +68,17 @@ class EncryptedMediaUrlImage(
|
||||
val encryptionNonce: ByteArray,
|
||||
) : MediaUrlImage(url, description, hash, blurhash, dim, uri, contentWarning, mimeType)
|
||||
|
||||
@Immutable
|
||||
open class MediaUrlPdf(
|
||||
url: String,
|
||||
description: String? = null,
|
||||
hash: String? = null,
|
||||
blurhash: String? = null,
|
||||
dim: DimensionTag? = null,
|
||||
uri: String? = null,
|
||||
mimeType: String? = null,
|
||||
) : MediaUrlContent(url, description, hash, dim, blurhash, uri, mimeType)
|
||||
|
||||
@Immutable
|
||||
open class MediaUrlVideo(
|
||||
url: String,
|
||||
|
||||
+36
-2
@@ -58,17 +58,21 @@ class RichTextParser {
|
||||
|
||||
val isImage: Boolean
|
||||
val isVideo: Boolean
|
||||
val isPdf: Boolean
|
||||
|
||||
if (contentType != null) {
|
||||
isImage = contentType.startsWith("image/")
|
||||
isVideo = contentType.startsWith("video/") || contentType.startsWith("audio/")
|
||||
isPdf = contentType.startsWith("application/pdf")
|
||||
} else if (fullUrl.startsWith("data:")) {
|
||||
isImage = fullUrl.startsWith("data:image/")
|
||||
isVideo = fullUrl.startsWith("data:video/") || fullUrl.startsWith("data:audio/")
|
||||
isPdf = fullUrl.startsWith("data:application/pdf")
|
||||
} else {
|
||||
val removedParamsFromUrl = removeQueryParamsForExtensionComparison(fullUrl)
|
||||
isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) }
|
||||
isVideo = videoExtensions.any { removedParamsFromUrl.endsWith(it) }
|
||||
isPdf = pdfExtensions.any { removedParamsFromUrl.endsWith(it) }
|
||||
}
|
||||
|
||||
return if (isImage) {
|
||||
@@ -93,6 +97,16 @@ class RichTextParser {
|
||||
uri = callbackUri,
|
||||
mimeType = contentType,
|
||||
)
|
||||
} else if (isPdf) {
|
||||
MediaUrlPdf(
|
||||
url = fullUrl,
|
||||
description = description ?: frags[AltTag.TAG_NAME] ?: tags[AltTag.TAG_NAME]?.firstOrNull(),
|
||||
hash = frags[HashSha256Tag.TAG_NAME] ?: tags[HashSha256Tag.TAG_NAME]?.firstOrNull(),
|
||||
blurhash = frags[BlurhashTag.TAG_NAME] ?: tags[BlurhashTag.TAG_NAME]?.firstOrNull(),
|
||||
dim = frags[DimensionTag.TAG_NAME]?.let { DimensionTag.parse(it) } ?: tags[DimensionTag.TAG_NAME]?.firstOrNull()?.let { DimensionTag.parse(it) },
|
||||
uri = callbackUri,
|
||||
mimeType = contentType,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -158,6 +172,7 @@ class RichTextParser {
|
||||
|
||||
val imageUrls = mediaForPager.filterValues { it is MediaUrlImage }.keys
|
||||
val videoUrls = mediaForPager.filterValues { it is MediaUrlVideo }.keys
|
||||
val pdfUrls = mediaForPager.filterValues { it is MediaUrlPdf }.keys
|
||||
|
||||
val emojiMap = CustomEmoji.createEmojiMap(tags.lists)
|
||||
|
||||
@@ -165,7 +180,7 @@ class RichTextParser {
|
||||
|
||||
val newContent = fixMissingSpaces(content, allUrls)
|
||||
|
||||
val segments = findTextSegments(newContent, imageUrls, videoUrls, urlSet, emojiMap, tags)
|
||||
val segments = findTextSegments(newContent, imageUrls, videoUrls, pdfUrls, urlSet, emojiMap, tags)
|
||||
|
||||
val mediaForPagerWithBase64 =
|
||||
mediaForPager +
|
||||
@@ -197,6 +212,7 @@ class RichTextParser {
|
||||
content: String,
|
||||
images: Set<String>,
|
||||
videos: Set<String>,
|
||||
pdfs: Set<String>,
|
||||
urls: Urls,
|
||||
emojis: Map<String, String>,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
@@ -211,7 +227,7 @@ class RichTextParser {
|
||||
|
||||
val segments = ArrayList<Segment>(wordList.size)
|
||||
wordList.forEach { word ->
|
||||
segments.add(wordIdentifier(word, images, videos, urls, emojis, tags))
|
||||
segments.add(wordIdentifier(word, images, videos, pdfs, urls, emojis, tags))
|
||||
}
|
||||
|
||||
paragraphSegments.add(ParagraphState(segments.toPersistentList(), isRTL))
|
||||
@@ -262,6 +278,7 @@ class RichTextParser {
|
||||
word: String,
|
||||
images: Set<String>,
|
||||
videos: Set<String>,
|
||||
pdfs: Set<String>,
|
||||
urls: Urls,
|
||||
emojis: Map<String, String>,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
@@ -288,6 +305,14 @@ class RichTextParser {
|
||||
}
|
||||
}
|
||||
|
||||
if (pdfs.contains(word)) {
|
||||
return if (urls.withoutScheme.contains(word)) {
|
||||
PdfSegment("https://$word")
|
||||
} else {
|
||||
PdfSegment(word)
|
||||
}
|
||||
}
|
||||
|
||||
if (urls.withoutScheme.contains(word)) return SchemelessUrlSegment(word)
|
||||
|
||||
if (urls.withScheme.contains(word)) return LinkSegment(word)
|
||||
@@ -377,9 +402,11 @@ class RichTextParser {
|
||||
|
||||
val imageExt = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg", "avif")
|
||||
val videoExt = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3", "m3u8", "ogg", "wav", "flac", "aac", "opus", "m4a")
|
||||
val pdfExt = listOf("pdf")
|
||||
|
||||
val imageExtensions = imageExt + imageExt.map { it.uppercase() }
|
||||
val videoExtensions = videoExt + videoExt.map { it.uppercase() }
|
||||
val pdfExtensions = pdfExt + pdfExt.map { it.uppercase() }
|
||||
|
||||
val tagIndex = Regex("\\#\\[([0-9]+)\\](.*)")
|
||||
val hashTagsPattern: Regex =
|
||||
@@ -421,6 +448,11 @@ class RichTextParser {
|
||||
return videoExtensions.any { removedParamsFromUrl.endsWith(it) }
|
||||
}
|
||||
|
||||
fun isPdfUrl(url: String): Boolean {
|
||||
val removedParamsFromUrl = removeQueryParamsForExtensionComparison(url)
|
||||
return pdfExtensions.any { removedParamsFromUrl.endsWith(it) }
|
||||
}
|
||||
|
||||
fun isValidURL(url: String?): Boolean =
|
||||
try {
|
||||
if (url != null) {
|
||||
@@ -496,4 +528,6 @@ val mimeTypeMap: Map<String, String> =
|
||||
"m4a" to "audio/mp4",
|
||||
"aac" to "audio/aac",
|
||||
"flac" to "audio/flac",
|
||||
// Documents
|
||||
"pdf" to "application/pdf",
|
||||
)
|
||||
|
||||
+5
@@ -62,6 +62,11 @@ class VideoSegment(
|
||||
segment: String,
|
||||
) : Segment(segment)
|
||||
|
||||
@Immutable
|
||||
class PdfSegment(
|
||||
segment: String,
|
||||
) : Segment(segment)
|
||||
|
||||
@Immutable
|
||||
class LinkSegment(
|
||||
segment: String,
|
||||
|
||||
Reference in New Issue
Block a user