diff --git a/app/src/main/java/com/capyreader/app/common/ArticlePrintHelper.kt b/app/src/main/java/com/capyreader/app/common/ArticlePrintHelper.kt new file mode 100644 index 000000000..190379c8a --- /dev/null +++ b/app/src/main/java/com/capyreader/app/common/ArticlePrintHelper.kt @@ -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 { + 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() +} diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleBottomBar.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleBottomBar.kt index 1ca5b56a1..1655069b1 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleBottomBar.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleBottomBar.kt @@ -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 @@ -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 @@ -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) +// ) +// } +// } } } } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleTopBar.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleTopBar.kt index 4c95bdceb..c3ebbb651 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleTopBar.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleTopBar.kt @@ -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 @@ -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(stiffness = 700f) @@ -50,11 +54,12 @@ private val sizeSpec = spring(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( @@ -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, @@ -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) ) { @@ -153,7 +173,6 @@ private fun ArticleTopBarPreview() { ArticleTopBar( show = true, isScrolled = false, - articleId = "", onClose = {} ) } diff --git a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt index 77bed3f38..3f385658a 100644 --- a/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt +++ b/app/src/main/java/com/capyreader/app/ui/articles/detail/ArticleView.kt @@ -177,7 +177,7 @@ fun ArticleView( ArticleTopBar( show = showToolBar, isScrolled = scrollState.showTopDivider, - articleId = article.id, + article = article, onClose = onBackPressed, ) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5f5feffb7..4e0936b5e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -50,6 +50,7 @@ %1$dhrs. %1$dd Compartir el artículo + Imprimir artículo Extraer todo el contenido Marcar todo como leído ¿Marcar todos los elementos como leídos? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0cc274fa..4b408ea81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,6 +57,7 @@ %1$dh %1$dd Share article + Print article Extract Full Content Mark All as Read Mark all items as read?