From 76e9be5f63fd7727a909db8c3bb58e2120d03ab2 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sun, 16 Feb 2025 21:09:26 +0100 Subject: [PATCH 1/3] Implement the sharing and opening in browser features on iOS. --- .../com/daniebeler/pfpixelix/AppViewController.kt | 15 ++++++++++----- .../domain/usecase/OpenExternalUrlUseCase.ios.kt | 2 ++ .../daniebeler/pfpixelix/utils/KmpPlatform.ios.kt | 9 ++++++++- .../com/daniebeler/pfpixelix/utils/Share.ios.kt | 4 ++++ 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt index bb08caa5..5ffd3b2b 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt @@ -9,14 +9,17 @@ import com.daniebeler.pfpixelix.utils.KmpContext import com.daniebeler.pfpixelix.utils.LocalKmpContext import platform.UIKit.UIViewController -private object IosContext : KmpContext() - class IosUrlCallback { var onRedirect: (String) -> Unit = {} } fun AppViewController(urlCallback: IosUrlCallback): UIViewController { - val appComponent = AppComponent.Companion.create(IosContext) + var viewController: UIViewController? = null + val context = object : KmpContext() { + override val viewController: UIViewController + get() = viewController!! + } + val appComponent = AppComponent.Companion.create(context) SingletonImageLoader.setSafe { appComponent.provideImageLoader() @@ -27,11 +30,13 @@ fun AppViewController(urlCallback: IosUrlCallback): UIViewController { } val finishApp = {} - return ComposeUIViewController { + viewController = ComposeUIViewController { CompositionLocalProvider( - LocalKmpContext provides IosContext + LocalKmpContext provides context ) { App(appComponent, finishApp) } } + + return viewController } \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/OpenExternalUrlUseCase.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/OpenExternalUrlUseCase.ios.kt index 14631aae..8731ded8 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/OpenExternalUrlUseCase.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/OpenExternalUrlUseCase.ios.kt @@ -2,10 +2,12 @@ package com.daniebeler.pfpixelix.domain.usecase import com.daniebeler.pfpixelix.domain.repository.StorageRepository import com.daniebeler.pfpixelix.utils.KmpContext +import com.daniebeler.pfpixelix.utils.openUrlInBrowser import me.tatarka.inject.annotations.Inject @Inject actual class OpenExternalUrlUseCase actual constructor(repository: StorageRepository) { actual operator fun invoke(url: String, context: KmpContext) { + context.openUrlInBrowser(url) } } \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.ios.kt index 0eedc328..3d145ef0 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.ios.kt @@ -11,9 +11,12 @@ import platform.Foundation.NSBundle import platform.Foundation.NSDictionary import platform.Foundation.NSDocumentDirectory import platform.Foundation.NSFileManager +import platform.Foundation.NSURL import platform.Foundation.NSUserDefaults import platform.Foundation.NSUserDomainMask import platform.Foundation.fileSize +import platform.UIKit.UIApplication +import platform.UIKit.UIViewController private data class IosUri(val uri: String) : KmpUri() { override fun toString(): String = uri @@ -23,7 +26,9 @@ actual abstract class KmpUri { actual abstract override fun toString(): String } -actual abstract class KmpContext +actual abstract class KmpContext { + abstract val viewController: UIViewController +} actual val KmpContext.dataStoreDir get() = appDocDir().resolve("dataStore") actual val KmpContext.imageCacheDir get() = appDocDir().resolve("imageCache") @@ -39,9 +44,11 @@ private fun appDocDir() = NSFileManager.defaultManager.URLForDirectory( )!!.path!!.toPath() actual fun KmpContext.openUrlInApp(url: String) { + openUrlInBrowser(url) } actual fun KmpContext.openUrlInBrowser(url: String) { + UIApplication.sharedApplication.openURL(NSURL(string = url)) } actual val KmpContext.pref: Settings diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/Share.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/Share.ios.kt index ba38f0fa..376bf159 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/Share.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/Share.ios.kt @@ -1,6 +1,10 @@ package com.daniebeler.pfpixelix.utils +import platform.UIKit.UIActivityViewController + actual object Share { actual fun shareText(context: KmpContext, text: String) { + val vc = UIActivityViewController(listOf(text), null) + context.viewController.presentViewController(vc, true, null) } } \ No newline at end of file From b283b19c124e6f4d393db1d1928d8fc360ac4fe8 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Mon, 17 Feb 2025 00:29:38 +0100 Subject: [PATCH 2/3] Implement media upload on iOS. --- app/build.gradle.kts | 3 + .../usecase/UploadMediaUseCase.android.kt | 24 ----- .../newpost/NewPostComposable.android.kt | 37 -------- .../pfpixelix/utils/KmpPlatform.android.kt | 6 +- .../pfpixelix/data/remote/PixelfedApi.kt | 3 +- .../repository/PostEditorRepositoryImpl.kt | 2 +- .../domain/usecase/UploadMediaUseCase.kt | 12 ++- .../composables/newpost/NewPostComposable.kt | 34 +++++-- .../daniebeler/pfpixelix/utils/KmpPlatform.kt | 3 + .../domain/usecase/UploadMediaUseCase.ios.kt | 20 ---- .../newpost/NewPostComposable.ios.kt | 8 -- .../daniebeler/pfpixelix/utils/GetFile.ios.kt | 12 ++- .../pfpixelix/utils/KmpMediaFile.ios.kt | 94 ++++++++++++++++--- .../pfpixelix/utils/KmpPlatform.ios.kt | 16 ++-- .../pfpixelix/utils/MimeType.ios.kt | 20 +++- gradle/libs.versions.toml | 4 +- 16 files changed, 170 insertions(+), 128 deletions(-) delete mode 100644 app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.android.kt delete mode 100644 app/src/androidMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.android.kt delete mode 100644 app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.ios.kt delete mode 100644 app/src/iosMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.ios.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index abc50ae7..786a3f03 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -66,6 +66,9 @@ kotlin { //shared preferences implementation(libs.multiplatform.settings) + //file picker + implementation(libs.filekit.compose) + //lifecycle implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel) diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.android.kt deleted file mode 100644 index 4667ab58..00000000 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.android.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.daniebeler.pfpixelix.domain.usecase - -import com.daniebeler.pfpixelix.common.Resource -import com.daniebeler.pfpixelix.domain.model.MediaAttachment -import com.daniebeler.pfpixelix.domain.repository.PostEditorRepository -import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.KmpUri -import kotlinx.coroutines.flow.Flow -import me.tatarka.inject.annotations.Inject - -@Inject -actual class UploadMediaUseCase actual constructor( - private val postEditorRepository: PostEditorRepository -) { - actual operator fun invoke( - url: KmpUri, - description: String, - context: KmpContext - ): Flow> { - return postEditorRepository.uploadMedia( - url, description, context - ) - } -} \ No newline at end of file diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.android.kt deleted file mode 100644 index a973f93d..00000000 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.android.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.daniebeler.pfpixelix.ui.composables.newpost - -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.daniebeler.pfpixelix.utils.KmpUri -import org.jetbrains.compose.resources.vectorResource -import pixelix.app.generated.resources.Res -import pixelix.app.generated.resources.add_outline - -@Composable -actual fun SinglePhotoPickerButton(selected: (result: List) -> Unit) { - val singlePhotoPickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickMultipleVisualMedia(), - onResult = { uris -> - selected(uris) - }) - Icon( - modifier = Modifier - .clickable { - singlePhotoPickerLauncher.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo) - ) - } - .height(50.dp) - .width(50.dp), - imageVector = vectorResource(Res.drawable.add_outline), - contentDescription = null, - ) -} \ No newline at end of file diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.android.kt index 0807dd31..f5cd9bd3 100644 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.android.kt +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.android.kt @@ -24,6 +24,7 @@ import com.daniebeler.pfpixelix.utils.ThemePrefUtil.LIGHT import com.daniebeler.pfpixelix.widget.notifications.NotificationWidgetReceiver import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings +import io.github.vinceglb.filekit.core.PlatformFile import okio.Path import okio.Path.Companion.toPath import java.io.File @@ -31,6 +32,7 @@ import java.io.File actual typealias KmpUri = Uri actual fun String.toKmpUri(): KmpUri = this.toUri() actual val EmptyKmpUri = Uri.EMPTY +actual fun PlatformFile.toKmpUri(): KmpUri = this.uri actual typealias KmpContext = Context @@ -144,4 +146,6 @@ actual fun KmpContext.pinWidget() { } actual fun isAbleToDownloadImage(): Boolean = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q \ No newline at end of file + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + +actual fun KmpUri.getPlatformUriObject(): Any = this \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/PixelfedApi.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/PixelfedApi.kt index bee58a4c..84c5dc9e 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/PixelfedApi.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/PixelfedApi.kt @@ -362,10 +362,9 @@ interface PixelfedApi { @Query("q") searchText: String ): Call> - @Headers("Content-Type: application/json") @POST("api/v2/media") fun uploadMedia( - @Body body: String + @Body body: MultiPartFormDataContent ): Call @FormUrlEncoded diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/PostEditorRepositoryImpl.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/PostEditorRepositoryImpl.kt index 7d447fb7..e46a6f9d 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/PostEditorRepositoryImpl.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/PostEditorRepositoryImpl.kt @@ -65,7 +65,7 @@ class PostEditorRepositoryImpl @Inject constructor( ) try { - val res = pixelfedApi.uploadMedia(json.encodeToString(data)).execute().toModel() + val res = pixelfedApi.uploadMedia(data).execute().toModel() emit(Resource.Success(res)) } catch (e: Exception) { Logger.d(e.message.toString()) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.kt index ddfb3854..1ad2bb62 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.kt @@ -9,12 +9,16 @@ import kotlinx.coroutines.flow.Flow import me.tatarka.inject.annotations.Inject @Inject -expect class UploadMediaUseCase( - postEditorRepository: PostEditorRepository +class UploadMediaUseCase( + private val postEditorRepository: PostEditorRepository ) { operator fun invoke( url: KmpUri, description: String, - context: KmpContext, - ): Flow> + context: KmpContext + ): Flow> { + return postEditorRepository.uploadMedia( + url, description, context + ) + } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.kt index dce75961..f27b794f 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.kt @@ -1,5 +1,6 @@ package com.daniebeler.pfpixelix.ui.composables.newpost +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -64,10 +65,16 @@ import com.daniebeler.pfpixelix.ui.composables.textfield_mentions.TextFieldMenti import com.daniebeler.pfpixelix.utils.KmpUri import com.daniebeler.pfpixelix.utils.LocalKmpContext import com.daniebeler.pfpixelix.utils.MimeType +import com.daniebeler.pfpixelix.utils.getPlatformUriObject import com.daniebeler.pfpixelix.utils.imeAwareInsets +import com.daniebeler.pfpixelix.utils.toKmpUri +import io.github.vinceglb.filekit.compose.rememberFilePickerLauncher +import io.github.vinceglb.filekit.core.PickerMode +import io.github.vinceglb.filekit.core.PickerType import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.add_outline import pixelix.app.generated.resources.alt_text import pixelix.app.generated.resources.audience import pixelix.app.generated.resources.audience_public @@ -134,13 +141,13 @@ fun NewPostComposable( if (type != null && type.take(5) == "video") { //todo KMP video AsyncImage( - model = image.imageUri, + model = image.imageUri.getPlatformUriObject(), contentDescription = "video thumbnail", modifier = Modifier.width(100.dp) ) } else { AsyncImage( - model = image.imageUri, + model = image.imageUri.getPlatformUriObject(), contentDescription = null, modifier = Modifier.width(100.dp) ) @@ -195,11 +202,22 @@ fun NewPostComposable( } } Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - SinglePhotoPickerButton { result -> - result.forEach { - viewModel.addImage(it, context) + val launcher = rememberFilePickerLauncher( + type = PickerType.ImageAndVideo, + mode = PickerMode.Multiple() + ) { files -> + files?.forEach { file -> + viewModel.addImage(file.toKmpUri(), context) } } + Icon( + modifier = Modifier + .clickable { launcher.launch() } + .height(50.dp) + .width(50.dp), + imageVector = vectorResource(Res.drawable.add_outline), + contentDescription = null, + ) } Spacer(modifier = Modifier.height(20.dp)) TextFieldMentionsComposable( @@ -300,11 +318,10 @@ fun NewPostComposable( } - } } TextFieldLocationsComposable( - submit = {viewModel.setLocation(it)}, + submit = { viewModel.setLocation(it) }, submitPlace = {}, initialValue = null, labelStringId = Res.string.location, @@ -359,6 +376,3 @@ fun NewPostComposable( } } } - -@Composable -expect fun SinglePhotoPickerButton(selected: (result: List) -> Unit) \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt index 3d8c2b8b..4394139a 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt @@ -4,13 +4,16 @@ import androidx.compose.runtime.staticCompositionLocalOf import coil3.PlatformContext import com.daniebeler.pfpixelix.ui.composables.settings.icon_selection.IconWithName import com.russhwolf.settings.Settings +import io.github.vinceglb.filekit.core.PlatformFile import okio.Path expect abstract class KmpUri { abstract override fun toString(): String } expect fun String.toKmpUri(): KmpUri +expect fun PlatformFile.toKmpUri(): KmpUri expect val EmptyKmpUri: KmpUri +expect fun KmpUri.getPlatformUriObject(): Any expect abstract class KmpContext val LocalKmpContext = staticCompositionLocalOf { error("no KmpContext") } diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.ios.kt deleted file mode 100644 index 0dd2cdf2..00000000 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UploadMediaUseCase.ios.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.daniebeler.pfpixelix.domain.usecase - -import com.daniebeler.pfpixelix.common.Resource -import com.daniebeler.pfpixelix.domain.model.MediaAttachment -import com.daniebeler.pfpixelix.domain.repository.PostEditorRepository -import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.KmpUri -import kotlinx.coroutines.flow.Flow -import me.tatarka.inject.annotations.Inject - -@Inject -actual class UploadMediaUseCase actual constructor(postEditorRepository: PostEditorRepository) { - actual operator fun invoke( - url: KmpUri, - description: String, - context: KmpContext - ): Flow> { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.ios.kt deleted file mode 100644 index 65da221f..00000000 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.ios.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.daniebeler.pfpixelix.ui.composables.newpost - -import androidx.compose.runtime.Composable -import com.daniebeler.pfpixelix.utils.KmpUri - -@Composable -actual fun SinglePhotoPickerButton(selected: (result: List) -> Unit) { -} \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/GetFile.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/GetFile.ios.kt index d4557623..f48f2fdd 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/GetFile.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/GetFile.ios.kt @@ -1,17 +1,25 @@ package com.daniebeler.pfpixelix.utils +import kotlinx.cinterop.ExperimentalForeignApi +import platform.Foundation.NSFileManager +import platform.Foundation.NSFileSize + actual object GetFile { actual fun getFileName( uri: KmpUri, context: KmpContext ): String? { - TODO("Not yet implemented") + return uri.url.lastPathComponent() } + @OptIn(ExperimentalForeignApi::class) actual fun getFileSize( uri: KmpUri, context: KmpContext ): Long? { - TODO("Not yet implemented") + val path = uri.url.path ?: return null + val fm = NSFileManager.defaultManager + val attr = fm.attributesOfItemAtPath(path, null) ?: return null + return attr.getValue(NSFileSize) as Long } } \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpMediaFile.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpMediaFile.ios.kt index 7ae0058d..265d0696 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpMediaFile.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpMediaFile.ios.kt @@ -1,23 +1,95 @@ package com.daniebeler.pfpixelix.utils +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.get +import kotlinx.cinterop.refTo +import kotlinx.cinterop.usePinned +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext +import platform.CoreFoundation.CFDataGetBytePtr +import platform.CoreFoundation.CFDataGetLength +import platform.CoreFoundation.CFDictionaryAddValue +import platform.CoreFoundation.CFDictionaryCreateMutable +import platform.CoreFoundation.CFRelease +import platform.CoreFoundation.CFURLRef +import platform.CoreGraphics.CGDataProviderCopyData +import platform.CoreGraphics.CGImageGetDataProvider +import platform.Foundation.CFBridgingRetain +import platform.Foundation.NSData +import platform.Foundation.NSNumber +import platform.Foundation.dataWithContentsOfURL +import platform.ImageIO.CGImageSourceCreateThumbnailAtIndex +import platform.ImageIO.CGImageSourceCreateWithURL +import platform.ImageIO.kCGImageSourceCreateThumbnailFromImageAlways +import platform.ImageIO.kCGImageSourceCreateThumbnailWithTransform +import platform.ImageIO.kCGImageSourceThumbnailMaxPixelSize +import platform.posix.memcpy + +@OptIn(ExperimentalForeignApi::class) actual class KmpMediaFile actual constructor( actual val uri: KmpUri, actual val context: KmpContext ) { - actual fun getMimeType(): String { - TODO("Not yet implemented") - } + private val fileName = GetFile.getFileName(uri, context) ?: error("file '$uri' not found") - actual suspend fun getBytes(): ByteArray { - TODO("Not yet implemented") - } + actual fun getMimeType(): String = MimeType.getMimeType(uri, context) ?: "image/*" - actual fun getName(): String { - TODO("Not yet implemented") + actual suspend fun getBytes(): ByteArray = withContext(Dispatchers.IO) { + val data = NSData.dataWithContentsOfURL(uri.url)!! + ByteArray(data.length.toInt()).apply { + data.usePinned { + memcpy(refTo(0), data.bytes, data.length) + } + } } - actual suspend fun getThumbnail(): ByteArray? { - TODO("Not yet implemented") - } + actual fun getName(): String = fileName + + actual suspend fun getThumbnail(): ByteArray? = withContext(Dispatchers.IO) { + val urlRef = CFBridgingRetain(uri.url) as CFURLRef + val imageSource = CGImageSourceCreateWithURL(urlRef, null)!! + val thumbnailOptions = CFDictionaryCreateMutable( + null, + 3, + null, + null + ).apply { + CFDictionaryAddValue( + this, + kCGImageSourceCreateThumbnailWithTransform, + CFBridgingRetain(NSNumber(bool = true)) + ) + CFDictionaryAddValue( + this, + kCGImageSourceCreateThumbnailFromImageAlways, + CFBridgingRetain(NSNumber(bool = true)) + ) + CFDictionaryAddValue( + this, + kCGImageSourceThumbnailMaxPixelSize, + CFBridgingRetain(NSNumber(512)) + ) + } + val thumbnailSource = CGImageSourceCreateThumbnailAtIndex( + imageSource, + 0u, + thumbnailOptions + ) + + val data = CGDataProviderCopyData(CGImageGetDataProvider(thumbnailSource)) + val bytePointer = CFDataGetBytePtr(data)!! + val length = CFDataGetLength(data) + + val byteArray = ByteArray(length.toInt()) { index -> + bytePointer[index].toByte() + } + + CFRelease(urlRef) + CFRelease(data) + CFRelease(thumbnailSource) + + byteArray + } } \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.ios.kt index 3d145ef0..114a08ba 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.ios.kt @@ -4,6 +4,7 @@ import coil3.PlatformContext import com.daniebeler.pfpixelix.ui.composables.settings.icon_selection.IconWithName import com.russhwolf.settings.NSUserDefaultsSettings import com.russhwolf.settings.Settings +import io.github.vinceglb.filekit.core.PlatformFile import kotlinx.cinterop.ExperimentalForeignApi import okio.Path import okio.Path.Companion.toPath @@ -18,11 +19,12 @@ import platform.Foundation.fileSize import platform.UIKit.UIApplication import platform.UIKit.UIViewController -private data class IosUri(val uri: String) : KmpUri() { - override fun toString(): String = uri +private data class IosUri(override val url: NSURL) : KmpUri() { + override fun toString(): String = url.toString() } actual abstract class KmpUri { + abstract val url: NSURL actual abstract override fun toString(): String } @@ -91,12 +93,14 @@ actual fun KmpContext.enableCustomIcon(iconWithName: IconWithName) { actual fun KmpContext.disableCustomIcon() { } -actual fun String.toKmpUri(): KmpUri = IosUri(this) - -actual val EmptyKmpUri: KmpUri = IosUri("") +actual fun String.toKmpUri(): KmpUri = IosUri(NSURL(string = this)) +actual fun PlatformFile.toKmpUri(): KmpUri = IosUri(nsUrl) +actual val EmptyKmpUri: KmpUri = IosUri(NSURL(string = "")) actual fun KmpContext.pinWidget() { } actual fun isAbleToDownloadImage(): Boolean { return false //TODO("Not yet implemented") -} \ No newline at end of file +} + +actual fun KmpUri.getPlatformUriObject(): Any = url \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/MimeType.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/MimeType.ios.kt index d83402f6..2569028d 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/MimeType.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/MimeType.ios.kt @@ -1,10 +1,28 @@ package com.daniebeler.pfpixelix.utils +import kotlinx.cinterop.ExperimentalForeignApi +import platform.CoreFoundation.CFRelease +import platform.CoreFoundation.CFStringRef +import platform.CoreServices.UTTypeCopyPreferredTagWithClass +import platform.CoreServices.UTTypeCreatePreferredIdentifierForTag +import platform.CoreServices.kUTTagClassFilenameExtension +import platform.CoreServices.kUTTagClassMIMEType +import platform.Foundation.CFBridgingRelease +import platform.Foundation.CFBridgingRetain +import platform.Foundation.NSString + actual object MimeType { + @OptIn(ExperimentalForeignApi::class) actual fun getMimeType( uri: KmpUri, context: KmpContext ): String? { - TODO("Not yet implemented") + val fileExtension = uri.url.pathExtension() + val fileExtensionRef = CFBridgingRetain(fileExtension as NSString) as CFStringRef + val uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtensionRef, null) + CFRelease(fileExtensionRef) + val mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType) + CFRelease(uti) + return CFBridgingRelease(mimeType) as String } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80e6c7f3..5cd4da3d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ navigationMultiplatform = "2.8.0+dev2049" kotlinx-coroutines = "1.10.1" kotlinxCollectionsImmutable = "0.3.5" kotlinxSerializationJson = "1.8.0" -ktor = "3.0.3" +ktor = "3.1.0" kotlinx-datetime = "0.6.1" #multiplatform @@ -25,6 +25,7 @@ androidx-annotation = "1.9.1" coil = "3.1.0" datastorePreferences = "1.1.2" multiplatformSettings = "1.3.0" +filekitCompose = "0.8.8" #android accompanistSystemuicontroller = "0.34.0" @@ -96,6 +97,7 @@ ksoup = { module = "com.fleeksoft.ksoup:ksoup", version.ref = "ksoup" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } +filekit-compose = { module = "io.github.vinceglb:filekit-compose", version.ref = "filekitCompose" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From 4c8308eb36f7d4365eb843bee6f071cf630c932b Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Mon, 17 Feb 2025 12:33:14 +0100 Subject: [PATCH 3/3] Clean log messages. --- app/build.gradle.kts | 12 ++++++++++++ .../kotlin/com/daniebeler/pfpixelix/MyApplication.kt | 2 ++ .../domain/usecase/UpdateAccountUseCase.android.kt | 2 +- .../kotlin/com/daniebeler/pfpixelix/App.kt | 4 ++-- .../data/remote/dto/nodeinfo/SoftwareSmallDto.kt | 2 +- .../com/daniebeler/pfpixelix/di/AppComponent.kt | 4 ++-- .../pfpixelix/domain/service/session/AuthService.kt | 2 +- .../ui/composables/mention/MentionComposable.kt | 2 +- .../profile/other_profile/OtherProfileViewModel.kt | 2 +- .../com/daniebeler/pfpixelix/utils/HtmlToText.kt | 2 +- .../kotlin/com/daniebeler/pfpixelix/utils/Logging.kt | 11 +++++++++++ .../com/daniebeler/pfpixelix/AppViewController.kt | 3 +++ 12 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Logging.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 786a3f03..a3172ca0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -142,6 +142,10 @@ android { } } + buildFeatures { + buildConfig = true + } + buildTypes { release { isMinifyEnabled = false @@ -151,6 +155,14 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } + + create("demo") { + initWith(getByName("debug")) + isMinifyEnabled = true + isDebuggable = false + isProfileable = false + isShrinkResources = true + } } packaging.resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt index 4bf0a463..f364f9ed 100644 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt @@ -10,6 +10,7 @@ import coil3.SingletonImageLoader import com.daniebeler.pfpixelix.di.AppComponent import com.daniebeler.pfpixelix.di.WorkerComponent import com.daniebeler.pfpixelix.di.create +import com.daniebeler.pfpixelix.utils.configureLogger import com.daniebeler.pfpixelix.widget.notifications.work_manager.LatestImageTask import com.daniebeler.pfpixelix.widget.notifications.work_manager.NotificationsTask @@ -25,6 +26,7 @@ class MyApplication : Application(), Configuration.Provider { SingletonImageLoader.setSafe { appComponent.provideImageLoader() } + configureLogger(BuildConfig.DEBUG) super.onCreate() } diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateAccountUseCase.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateAccountUseCase.android.kt index 30529121..05f58b53 100644 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateAccountUseCase.android.kt +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateAccountUseCase.android.kt @@ -37,7 +37,7 @@ actual class UpdateAccountUseCase actual constructor( append(HttpHeaders.ContentDisposition, fileName) }) } catch (e: Exception) { - Logger.e("UpdateAccountUseCase") { e.message!! } + Logger.e("UpdateAccountUseCase", e) } } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt index 9f72bbbe..2f5cf963 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt @@ -375,8 +375,8 @@ private fun NavGraphBuilder.navigationGraph( val uId = navBackStackEntry.arguments?.getString("postid") val refresh = navBackStackEntry.arguments?.getBoolean("refresh")!! val openReplies = navBackStackEntry.arguments?.getBoolean("openReplies")!! - Logger.d("refresh") { refresh.toString() } - Logger.d("openReplies") { openReplies.toString() } + Logger.d { "refresh $refresh" } + Logger.d { "openReplies $openReplies" } uId?.let { id -> SinglePostComposable(navController, postId = id, refresh, openReplies) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/dto/nodeinfo/SoftwareSmallDto.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/dto/nodeinfo/SoftwareSmallDto.kt index 14877299..f5679972 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/dto/nodeinfo/SoftwareSmallDto.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/dto/nodeinfo/SoftwareSmallDto.kt @@ -15,7 +15,7 @@ data class SoftwareSmallDto( @SerialName("version") val version: String ): DtoInterface { override fun toModel(): SoftwareSmall { - Logger.d("SoftwareSmallDto") { "Converting SoftwareSmallDto to SoftwareSmall: $this" } + Logger.d { "SoftwareSmallDto: Converting SoftwareSmallDto to SoftwareSmall: $this" } return SoftwareSmall( id = id, name = name, diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt index fe676253..717a320c 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt @@ -95,12 +95,12 @@ abstract class AppComponent( install(Logging) { logger = object : io.ktor.client.plugins.logging.Logger { override fun log(message: String) { - Logger.v("HttpClient") { + Logger.v("Pixelix HttpClient") { message.lines().joinToString { "\n\t\t$it"} } } } - level = LogLevel.BODY + level = LogLevel.INFO } install(HttpTimeout) { requestTimeoutMillis = 60000 diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthService.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthService.kt index d1642983..f7e8d872 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthService.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthService.kt @@ -134,7 +134,7 @@ class AuthService( install(Logging) { logger = object : io.ktor.client.plugins.logging.Logger { override fun log(message: String) { - Logger.v("HttpAuth") { + Logger.v("Pixelix HttpAuth") { message.lines().joinToString { "\n\t\t$it" } } } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/mention/MentionComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/mention/MentionComposable.kt index 9be6cd7e..4331e4a1 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/mention/MentionComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/mention/MentionComposable.kt @@ -67,7 +67,7 @@ fun MentionComposable( LaunchedEffect(viewModel.postState.post, viewModel.postContextState.postContext) { if (viewModel.postState.post != null && viewModel.postContextState.postContext != null) { val index = viewModel.postContextState.postContext!!.ancestors.size + 1 - Logger.d("index") { index.toString() } + Logger.d { "index $index" } coroutineScope.launch { lazyListState.scrollToItem(index) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileViewModel.kt index e2e79c31..610fd16e 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileViewModel.kt @@ -87,7 +87,7 @@ class OtherProfileViewModel @Inject constructor( } fun loadDataByUsername(username: String, refreshing: Boolean) { - Logger.d("byUsername") { "load data by username" } + Logger.d { "byUsername: load data by username" } getAccountByUsername(username, refreshing) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/HtmlToText.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/HtmlToText.kt index 2ae5ad3c..98ba285a 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/HtmlToText.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/HtmlToText.kt @@ -14,7 +14,7 @@ object HtmlToText { val text = document.text().replace("\\n", "\n") val cleanedText = text.lines().joinToString("\n") { it.trimStart() } // Trim leading spaces - Logger.d("htmlToText") { cleanedText } + Logger.d { cleanedText } return cleanedText.trim() } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Logging.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Logging.kt new file mode 100644 index 00000000..4d64e864 --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Logging.kt @@ -0,0 +1,11 @@ +package com.daniebeler.pfpixelix.utils + +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity + +fun configureLogger(isDebug: Boolean = false) { + Logger.setTag("Pixelix") + Logger.setMinSeverity( + if (isDebug) Severity.Verbose else Severity.Error + ) +} \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt index 5ffd3b2b..982923ad 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt @@ -7,6 +7,7 @@ import com.daniebeler.pfpixelix.di.AppComponent import com.daniebeler.pfpixelix.di.create import com.daniebeler.pfpixelix.utils.KmpContext import com.daniebeler.pfpixelix.utils.LocalKmpContext +import com.daniebeler.pfpixelix.utils.configureLogger import platform.UIKit.UIViewController class IosUrlCallback { @@ -21,6 +22,8 @@ fun AppViewController(urlCallback: IosUrlCallback): UIViewController { } val appComponent = AppComponent.Companion.create(context) + configureLogger() + SingletonImageLoader.setSafe { appComponent.provideImageLoader() }