Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions app/src/main/java/com/capyreader/app/common/ArticlePrintHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.capyreader.app.common

import android.content.Context
import android.net.Uri
import android.print.PrintAttributes
import android.print.PrintManager
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.webkit.WebViewAssetLoader
import com.capyreader.app.ui.articles.detail.byline
import com.jocmp.capy.Article
import com.jocmp.capy.articles.ArticleRenderer
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

/**
* Helper class to print article content using Android's Print Framework.
* Uses the same ArticleRenderer and styles as the article reader view.
*/
class ArticlePrintHelper(
private val context: Context,
private val article: Article,
) : KoinComponent {
private val renderer: ArticleRenderer by inject()

fun printArticle() {
// Create asset loader for loading stylesheets and fonts
val assetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(context))
.addPathHandler("/res/", WebViewAssetLoader.ResourcesPathHandler(context))
.build()

// Create a WebView for printing
val webView = WebView(context).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
builtInZoomControls = false
mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}
}

webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
// Intercept asset requests to load local files
val asset = assetLoader.shouldInterceptRequest(request.url)
if (asset != null) {
val headers = asset.responseHeaders ?: mutableMapOf()
headers["Access-Control-Allow-Origin"] = "*"
asset.responseHeaders = headers
return asset
}
return super.shouldInterceptRequest(view, request)
}

override fun onPageFinished(view: WebView, url: String) {
createWebPrintJob(view)
}
}

// Use the same renderer as the article reader with print-optimized colors
val htmlContent = renderer.render(
article = article,
byline = article.byline(context),
colors = getPrintColors(),
hideImages = false,
)

webView.loadDataWithBaseURL(
"https://appassets.androidplatform.net",
htmlContent,
"text/html",
"UTF-8",
null
)
}

private fun createWebPrintJob(webView: WebView) {
val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager

// Create a print adapter from the WebView
val printAdapter = webView.createPrintDocumentAdapter(article.title)

// Create a print job with the name and adapter
val jobName = "${context.packageName} - ${article.title}"

// Pass null to use system defaults and let the user choose in the print dialog
printManager.print(jobName, printAdapter, null)
}

companion object {
/**
* Returns colors optimized for printing (light background, dark text)
*/
fun getPrintColors(): Map<String, String> {
return mapOf(
"color_primary" to toHex(Color.Black),
"color_surface" to toHex(Color.White),
"color_surface_container_highest" to toHex(Color(0xFFF5F5F5)),
"color_on_surface" to toHex(Color.Black),
"color_on_surface_variant" to toHex(Color(0xFF666666)),
"color_surface_variant" to toHex(Color(0xFFEEEEEE)),
"color_primary_container" to toHex(Color(0xFFF0F0F0)),
"color_on_primary_container" to toHex(Color.Black),
"color_secondary" to toHex(Color(0xFF444444)),
"color_surface_container" to toHex(Color(0xFFFAFAFA)),
"color_surface_tint" to toHex(Color(0xFFE0E0E0)),
)
}

private fun toHex(color: Color): String {
val argb = color.toArgb()
return String.format("#%06X", 0xFFFFFF and argb)
}
}
}

/**
* Extension function to make printing articles easier from a Context
*/
fun Context.printArticle(article: Article) {
ArticlePrintHelper(this, article).printArticle()
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.Share
//import androidx.compose.material.icons.rounded.Print
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material3.ExperimentalMaterial3Api
Expand All @@ -39,6 +40,7 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.capyreader.app.R
//import com.capyreader.app.common.printArticle
import com.capyreader.app.common.shareArticle
import com.capyreader.app.ui.articles.FullContentLoadingIcon
import com.capyreader.app.ui.components.ToolbarTooltip
Expand Down Expand Up @@ -168,6 +170,20 @@ fun ArticleBottomBar(
)
}
}
// ToolbarTooltip(
// positioning = TooltipAnchorPosition.Above,
// message = stringResource(R.string.article_print)
// ) {
// IconButton(
// onClick = { context.printArticle(article = article) },
// ) {
// Icon(
// Icons.Rounded.Print,
// contentDescription = stringResource(R.string.article_print),
// modifier = Modifier.size(24.dp)
// )
// }
// }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.statusBars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.outlined.FormatSize
import androidx.compose.material.icons.rounded.Print
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -33,15 +34,18 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.capyreader.app.R
import com.capyreader.app.common.printArticle
import com.capyreader.app.ui.articles.LocalLabelsActions
import com.capyreader.app.ui.components.ToolbarTooltip
import com.capyreader.app.ui.fixtures.PreviewKoinApplication
import com.jocmp.capy.Article

private val sizeSpec = spring<IntSize>(stiffness = 700f)

Expand All @@ -50,11 +54,12 @@ private val sizeSpec = spring<IntSize>(stiffness = 700f)
fun ArticleTopBar(
show: Boolean,
isScrolled: Boolean,
articleId: String,
article: Article? = null,
onClose: () -> Unit,
) {
val containerColor = MaterialTheme.colorScheme.surface
val labelsActions = LocalLabelsActions.current
val context = LocalContext.current
val (isStyleSheetOpen, setStyleSheetOpen) = rememberSaveable { mutableStateOf(false) }

Box(
Expand Down Expand Up @@ -88,12 +93,12 @@ fun ArticleTopBar(
},
title = {},
actions = {
if (labelsActions.showLabels) {
if (labelsActions.showLabels && article != null) {
ToolbarTooltip(
message = stringResource(R.string.freshrss_article_actions_label)
) {
IconButton(
onClick = { labelsActions.openSheet(articleId) },
onClick = { labelsActions.openSheet(article.id) },
) {
Icon(
Icons.AutoMirrored.Outlined.Label,
Expand All @@ -103,6 +108,21 @@ fun ArticleTopBar(
}
}
}
if (article != null) {
ToolbarTooltip(
message = stringResource(R.string.article_print)
) {
IconButton(
onClick = { context.printArticle(article = article) },
) {
Icon(
Icons.Rounded.Print,
contentDescription = stringResource(R.string.article_print),
modifier = Modifier.size(24.dp)
)
}
}
}
ToolbarTooltip(
message = stringResource(R.string.article_style_options)
) {
Expand Down Expand Up @@ -153,7 +173,6 @@ private fun ArticleTopBarPreview() {
ArticleTopBar(
show = true,
isScrolled = false,
articleId = "",
onClose = {}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ fun ArticleView(
ArticleTopBar(
show = showToolBar,
isScrolled = scrollState.showTopDivider,
articleId = article.id,
article = article,
onClose = onBackPressed,
)

Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<string name="relative_time_hours">%1$dhrs.</string>
<string name="relative_time_days">%1$dd</string>
<string name="article_share">Compartir el artículo</string>
<string name="article_print">Imprimir artículo</string>
<string name="extract_full_content">Extraer todo el contenido</string>
<string name="action_mark_all_read">Marcar todo como leído</string>
<string name="mark_all_read_dialog_description">¿Marcar todos los elementos como leídos?</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<string name="relative_time_hours">%1$dh</string>
<string name="relative_time_days">%1$dd</string>
<string name="article_share">Share article</string>
<string name="article_print">Print article</string>
<string name="extract_full_content">Extract Full Content</string>
<string name="action_mark_all_read">Mark All as Read</string>
<string name="mark_all_read_dialog_description">Mark all items as read?</string>
Expand Down