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
2 changes: 2 additions & 0 deletions cmp-android/dependencies/demoDebugRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0
org.jetbrains.kotlinx:kotlinx-html:0.12.0
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0
Expand Down
2 changes: 2 additions & 0 deletions cmp-android/dependencies/demoReleaseRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0
org.jetbrains.kotlinx:kotlinx-html:0.12.0
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0
Expand Down
2 changes: 2 additions & 0 deletions cmp-android/dependencies/prodDebugRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0
org.jetbrains.kotlinx:kotlinx-html:0.12.0
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0
Expand Down
2 changes: 2 additions & 0 deletions cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0
org.jetbrains.kotlinx:kotlinx-html:0.12.0
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2026 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.designsystem.component

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.mifos.core.designsystem.theme.DesignToken
import template.core.base.designsystem.theme.KptTheme

@Composable
fun MifosTableRow(
cells: List<@Composable () -> Unit>,
widths: List<Dp>,
modifier: Modifier = Modifier,
edgeOffset: Dp = 0.dp,
backgroundColor: Color = KptTheme.colorScheme.surfaceVariant,
cornerShape: Shape = DesignToken.shapes.none,
showTopBorder: Boolean = false,
showBottomBorder: Boolean = true,
showSideBorders: Boolean = true,
borderColor: Color = KptTheme.colorScheme.outlineVariant,
onClick: () -> Unit = {},
) {
Column(
modifier = modifier.fillMaxWidth(),
) {
if (showTopBorder) {
HorizontalDivider(
modifier = Modifier
.padding(horizontal = edgeOffset),
thickness = 0.5.dp,
color = borderColor,
)
}
Row(
modifier = Modifier
.height(IntrinsicSize.Min)
.padding(horizontal = edgeOffset)
.clip(cornerShape)
.background(backgroundColor)
.clickable { onClick() },
) {
cells.forEachIndexed { index, content ->
val cellWidth = widths.getOrElse(index) { DesignToken.sizes.tableCellWidthMedium }
if (showSideBorders) {
VerticalDivider(
thickness = 0.5.dp,
color = borderColor,
)
}
Box(modifier = Modifier.width(cellWidth)) {
content()
if (showBottomBorder) {
HorizontalDivider(
modifier = Modifier.align(Alignment.BottomStart),
thickness = 0.5.dp,
color = borderColor,
)
}
}
if (showSideBorders && index == cells.lastIndex) {
VerticalDivider(
thickness = 0.5.dp,
color = borderColor,
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.FlashOff
import androidx.compose.material.icons.filled.FlashOn
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.IosShare
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Lock
Expand Down Expand Up @@ -248,4 +249,6 @@ object MifosIcons {
val Copy = Icons.Outlined.ContentCopy
val Currency = Icons.Outlined.CurrencyExchange
val Camera = Icons.Outlined.Camera

val Export = Icons.Default.IosShare
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ data class AppShapes(
val topCornerDp8: Shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
val topCornerDp16: Shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
val bottomCornerDp12: Shape = RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp),
val bottomCornerDp8: Shape = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp),
val dp2: Shape = RoundedCornerShape(2.dp),
val dp10: Shape = RoundedCornerShape(10.dp),
val dp22: Shape = RoundedCornerShape(22.dp),
Expand Down Expand Up @@ -360,6 +361,9 @@ data class AppSizes(
val logoSizeTopAppBar: Dp = 28.dp,
val topAppBarHeight: Dp = 85.dp,
val bottomAppBarHeight: Dp = 70.dp,
val tableCellWidthSmall: Dp = 60.dp,
val tableCellWidthMedium: Dp = 100.dp,
val tableCellWidthLarge: Dp = 150.dp,
val iconDp39: Dp = 39.dp,
val iconDp100: Dp = 100.dp,
val checkboxDp18: Dp = 18.dp,
Expand Down Expand Up @@ -412,9 +416,6 @@ data class AppSizes(
val dp100: Dp = 100.dp,
val dp120: Dp = 120.dp,
val dp128: Dp = 128.dp,
val tableCellWidthSmall: Dp = 65.dp,
val tableCellWidthMedium: Dp = 100.dp,
val tableCellWidthLarge: Dp = 150.dp,
)

@Immutable
Expand Down
5 changes: 5 additions & 0 deletions core/ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ kotlin{
implementation(libs.crop.krop.ui)
implementation(libs.compottie.resources)
implementation(libs.compottie.lite)
implementation(libs.kotlinx.html)
}
desktopMain.dependencies {
implementation(libs.openhtmltopdf.pdfbox)
implementation(libs.openhtmltopdf.svg.support)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2026 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.ui.util.pdf

import android.content.Context
import android.print.PrintAttributes
import android.print.PrintManager
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.content.getSystemService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
* Android implementation of PDF generator using WebView print adapter.
*/
actual class PdfGenerator {

private var context: Context? = null

fun setContext(context: Context) {
this.context = context
}

actual suspend fun generateAndSharePdf(
htmlContent: String,
fileName: String,
pageConfig: PageConfig,
) {
withContext(Dispatchers.Main) {
val appContext = context ?: throw Exception("Context not initialized")

val finalHtml = run {
val orientation = if (pageConfig.orientation == Orientation.LANDSCAPE) {
"landscape"
} else {
"portrait"
}
val size = when (pageConfig.size) {
PageSize.A4 -> "A4"
PageSize.LETTER -> "letter"
}
val pageCss =
"@page { size: $size $orientation; margin: ${pageConfig.marginMm}mm; }"
htmlContent.replace("/* PAGE_CONFIG_PLACEHOLDER */", pageCss)
}

var webView: WebView? = WebView(appContext)

try {
suspendCancellableCoroutine { continuation ->
val currentWebView = webView ?: return@suspendCancellableCoroutine

continuation.invokeOnCancellation {
currentWebView.stopLoading()
currentWebView.destroy()
webView = null
}

currentWebView.settings.javaScriptEnabled = false
currentWebView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
try {
val printManager = appContext.getSystemService<PrintManager>()
?: throw Exception("PrintManager not available")

val attributes = createPrintAttributes(pageConfig)
val nativeAdapter = view?.createPrintDocumentAdapter(fileName)
?: throw Exception("PrintDocumentAdapter is null")

printManager.print(fileName, nativeAdapter, attributes)

if (continuation.isActive) continuation.resume(Unit)
} catch (e: Exception) {
if (continuation.isActive) continuation.resumeWithException(e)
}
}

override fun onReceivedError(
view: WebView?,
errorCode: Int,
description: String?,
failingUrl: String?,
) {
if (continuation.isActive) {
continuation.resumeWithException(Exception("WebView Error: $description"))
}
}
}

currentWebView.loadDataWithBaseURL(null, finalHtml, "text/html", "UTF-8", null)
}
} finally {
// If webView wasn't nulled by cancellation, clean it up now
webView?.let { activeView ->
activeView.post {
activeView.stopLoading()
activeView.destroy()
}
webView = null
}
}
}
}

private fun createPrintAttributes(pageConfig: PageConfig): PrintAttributes {
val isLandscape = pageConfig.orientation == Orientation.LANDSCAPE
val mediaSize = when (pageConfig.size) {
PageSize.A4 -> if (isLandscape) {
PrintAttributes.MediaSize.ISO_A4.asLandscape()
} else {
PrintAttributes.MediaSize.ISO_A4
}

PageSize.LETTER -> if (isLandscape) {
PrintAttributes.MediaSize.NA_LETTER.asLandscape()
} else {
PrintAttributes.MediaSize.NA_LETTER
}
}
val marginMils = (pageConfig.marginMm * 39.3701).toInt()

return PrintAttributes.Builder()
.setMediaSize(mediaSize)
.setColorMode(PrintAttributes.COLOR_MODE_COLOR)
.setMinMargins(
PrintAttributes.Margins(
marginMils,
marginMils,
marginMils,
marginMils,
),
)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2026 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.ui.util.pdf

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext

/**
* Android-specific helper to create PdfGenerator with Context.
*/
@Composable
actual fun rememberPdfGenerator(): PdfGenerator {
val context = LocalContext.current
return remember(context) {
PdfGenerator().apply {
setContext(context)
}
}
}
Loading
Loading