diff --git a/composeApp/src/commonMain/kotlin/com/github/familyvault/utils/MimeTypeParser.kt b/composeApp/src/commonMain/kotlin/com/github/familyvault/utils/MimeTypeParser.kt new file mode 100644 index 00000000..815d675b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/github/familyvault/utils/MimeTypeParser.kt @@ -0,0 +1,109 @@ +package com.github.familyvault.utils + +object MimeTypeParser { + private val mimeTypes: Map = mapOf( + "html" to "text/html", + "htm" to "text/html", + "shtml" to "text/html", + "css" to "text/css", + "xml" to "text/xml", + "gif" to "image/gif", + "jpeg" to "image/jpeg", + "jpg" to "image/jpeg", + "js" to "application/javascript", + "atom" to "application/atom+xml", + "rss" to "application/rss+xml", + "mml" to "text/mathml", + "txt" to "text/plain", + "jad" to "text/vnd.sun.j2me.app-descriptor", + "wml" to "text/vnd.wap.wml", + "htc" to "text/x-component", + "png" to "image/png", + "tif" to "image/tiff", + "tiff" to "image/tiff", + "wbmp" to "image/vnd.wap.wbmp", + "ico" to "image/x-icon", + "jng" to "image/x-jng", + "bmp" to "image/x-ms-bmp", + "svg" to "image/svg+xml", + "svgz" to "image/svg+xml", + "webp" to "image/webp", + "woff" to "application/font-woff", + "jar" to "application/java-archive", + "war" to "application/java-archive", + "ear" to "application/java-archive", + "json" to "application/json", + "hqx" to "application/mac-binhex40", + "doc" to "application/msword", + "pdf" to "application/pdf", + "ps" to "application/postscript", + "eps" to "application/postscript", + "ai" to "application/postscript", + "rtf" to "application/rtf", + "m3u8" to "application/vnd.apple.mpegurl", + "xls" to "application/vnd.ms-excel", + "eot" to "application/vnd.ms-fontobject", + "ppt" to "application/vnd.ms-powerpoint", + "wmlc" to "application/vnd.wap.wmlc", + "kml" to "application/vnd.google-earth.kml+xml", + "kmz" to "application/vnd.google-earth.kmz", + "7z" to "application/x-7z-compressed", + "cco" to "application/x-cocoa", + "jardiff" to "application/x-java-archive-diff", + "jnlp" to "application/x-java-jnlp-file", + "run" to "application/x-makeself", + "pl" to "application/x-perl", + "pm" to "application/x-perl", + "prc" to "application/x-pilot", + "pdb" to "application/x-pilot", + "rar" to "application/x-rar-compressed", + "rpm" to "application/x-redhat-package-manager", + "sea" to "application/x-sea", + "swf" to "application/x-shockwave-flash", + "sit" to "application/x-stuffit", + "tcl" to "application/x-tcl", + "tk" to "application/x-tcl", + "der" to "application/x-x509-ca-cert", + "pem" to "application/x-x509-ca-cert", + "crt" to "application/x-x509-ca-cert", + "xpi" to "application/x-xpinstall", + "xhtml" to "application/xhtml+xml", + "xspf" to "application/xspf+xml", + "zip" to "application/zip", + "docx" to "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "xlsx" to "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "pptx" to "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "mid" to "audio/midi", + "midi" to "audio/midi", + "kar" to "audio/midi", + "mp3" to "audio/mpeg", + "ogg" to "audio/ogg", + "m4a" to "audio/x-m4a", + "ra" to "audio/x-realaudio", + "3gpp" to "video/3gpp", + "3gp" to "video/3gpp", + "ts" to "video/mp2t", + "mp4" to "video/mp4", + "mpeg" to "video/mpeg", + "mpg" to "video/mpeg", + "mov" to "video/quicktime", + "webm" to "video/webm", + "flv" to "video/x-flv", + "m4v" to "video/x-m4v", + "mng" to "video/x-mng", + "asx" to "video/x-ms-asf", + "asf" to "video/x-ms-asf", + "wmv" to "video/x-ms-wmv", + "avi" to "video/x-msvideo", + "pmxtt" to "application/x-stt", + "pmxmm" to "application/x-smm", + "pmxaa" to "audio/webm;codecs=opus", + "opus" to "audio/opus" + ) + + fun getMimeType(extension: String): String { + val type = mimeTypes.get(extension.lowercase()) + if (type != null) return type + else return "application/octet-stream" + } +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/com/github/familyvault/services/DocumentPickerService.kt b/composeApp/src/iosMain/kotlin/com/github/familyvault/services/DocumentPickerService.kt index 040be643..8a0c393b 100644 --- a/composeApp/src/iosMain/kotlin/com/github/familyvault/services/DocumentPickerService.kt +++ b/composeApp/src/iosMain/kotlin/com/github/familyvault/services/DocumentPickerService.kt @@ -1,7 +1,35 @@ package com.github.familyvault.services import androidx.compose.runtime.mutableStateListOf -import androidx.compose.ui.graphics.ImageBitmap +import com.github.familyvault.utils.MimeTypeParser +import io.ktor.utils.io.core.toByteArray +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.runBlocking +import platform.CoreGraphics.CGContextScaleCTM +import platform.CoreGraphics.CGContextTranslateCTM +import platform.Foundation.NSData +import platform.Foundation.NSURL +import platform.Foundation.dataWithContentsOfURL +import platform.PDFKit.PDFDocument +import platform.PDFKit.kPDFDisplayBoxMediaBox +import platform.UIKit.UIApplication +import platform.UIKit.UIColor +import platform.UIKit.UIDocumentPickerDelegateProtocol +import platform.UIKit.UIDocumentPickerViewController +import platform.UIKit.UIGraphicsImageRenderer +import platform.UIKit.UIImageJPEGRepresentation +import platform.UniformTypeIdentifiers.UTTypeArchive +import platform.UniformTypeIdentifiers.UTTypeImage +import platform.UniformTypeIdentifiers.UTTypeMovie +import platform.UniformTypeIdentifiers.UTTypePDF +import platform.UniformTypeIdentifiers.UTTypePresentation +import platform.UniformTypeIdentifiers.UTTypeSpreadsheet +import platform.UniformTypeIdentifiers.UTTypeText +import platform.darwin.NSObject import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -10,49 +38,145 @@ class DocumentPickerService : IDocumentPickerService { private var continuation: Continuation>? = null private val selectedDocumentUrls = mutableStateListOf() private var isInitialized = false + private var documentPickerViewController: UIDocumentPickerViewController? = null + private val pickerController = DocumentPickerController(selectedDocumentUrls) { + continuation?.resume(getSelectedDocumentAsByteArrays()) + continuation = null + documentPickerViewController = null + } companion object { private const val TAG = "DocumentPickerService" } fun initializeWithActivity() { - TODO("Method not yet implemented") } override fun openDocumentPicker() { - TODO("Method not yet implemented") + if (documentPickerViewController == null) documentPickerViewController = + UIDocumentPickerViewController( + forOpeningContentTypes = listOf( + UTTypeSpreadsheet, + UTTypePresentation, + UTTypePDF, + UTTypeText, + UTTypeMovie, + UTTypeArchive, + UTTypeImage + ) + ) + documentPickerViewController?.allowsMultipleSelection = true + documentPickerViewController?.setDelegate(pickerController) + UIApplication.sharedApplication.keyWindow?.rootViewController?.presentViewController( + documentPickerViewController!!, + true, + null + ) } override suspend fun pickDocumentsAndReturnByteArrays(): List = - TODO("Method not yet implemented") + suspendCoroutine { cont -> + continuation = cont + try { + openDocumentPicker() + } catch (e: Exception) { + cont.resume(emptyList()) + } + } override fun getBytesFromUri(uriString: String): ByteArray? { - TODO("Method not yet implemented") + return selectedDocumentUrls.firstOrNull { uri -> + uri == uriString + }.let { it -> + return@let runBlocking(Dispatchers.IO) { + suspendCoroutine { cont -> + cont.resume(it?.toByteArray()) + } + } + } } override fun getSelectedDocumentAsByteArrays(): List { - TODO("Method not yet implemented") + return selectedDocumentUrls.map { url -> + runBlocking { + suspendCoroutine { cont -> + val data: NSData = NSData.dataWithContentsOfURL(NSURL.fileURLWithPath(url))!! + cont.resume(data.toByteArray()) + } + } + } } - override fun getSelectedDocumentUrls(): List = selectedDocumentUrls + override fun getSelectedDocumentUrls(): List { + return selectedDocumentUrls + } override fun clearSelectedDocuments() { - TODO("Method not yet implemented") + selectedDocumentUrls.clear() } override fun removeSelectedDocument(uri: String) { - TODO("Method not yet implemented") + selectedDocumentUrls.remove(uri) } override fun getDocumentNameFromUri(uriString: String): String? { - TODO("Method not yet implemented") + return NSURL.fileURLWithPath(uriString).lastPathComponent() } override fun getDocumentMimeTypeFromUri(uriString: String): String? { - TODO("Method not yet implemented") + return MimeTypeParser.getMimeType( + NSURL.fileURLWithPath(uriString).pathExtension().toString() + ) } + @OptIn(ExperimentalForeignApi::class) override fun getDocumentPreviewPageFromUri(uriString: String): ByteArray { - TODO("Method not yet implemented") + val url = NSURL.fileURLWithPath(uriString) + val pdf = PDFDocument(url) + val page = pdf.pageAtIndex(0u) + val bounds = page?.boundsForBox(kPDFDisplayBoxMediaBox) + + val renderer = UIGraphicsImageRenderer(bounds!!) + val image = renderer.imageWithActions { it -> + UIColor.whiteColor.set() + it?.fillRect(page.boundsForBox(kPDFDisplayBoxMediaBox)) + + val context = it!!.CGContext + CGContextTranslateCTM(context, 0.0, (bounds.useContents { size.height })) + CGContextScaleCTM(context, 1.0, (-1.0)) + page.drawWithBox(kPDFDisplayBoxMediaBox, context) + } + + val data = UIImageJPEGRepresentation(image, 0.95) + return data?.toByteArray()!! + } +} + +@OptIn(BetaInteropApi::class) +class DocumentPickerController( + val selectedUris: MutableList, + val onFinish: () -> Unit +) : NSObject(), + UIDocumentPickerDelegateProtocol { + + override fun documentPicker( + controller: UIDocumentPickerViewController, + didPickDocumentsAtURLs: List<*> + ) { + val pickedDocumentsURLs = + didPickDocumentsAtURLs.filterIsInstance().map { it.path ?: "" } + + selectedUris.clear() + selectedUris.addAll(pickedDocumentsURLs) + controller.dismissViewControllerAnimated(true) { + onFinish() + } + } + + override fun documentPickerWasCancelled(controller: UIDocumentPickerViewController) { + selectedUris.clear() + controller.dismissViewControllerAnimated(true) { + onFinish() + } } }