diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml
index eb909059..4c984a94 100644
--- a/.github/workflows/crowdin.yml
+++ b/.github/workflows/crowdin.yml
@@ -4,7 +4,11 @@ on:
push:
branches: [ dev ]
workflow_dispatch:
-
+
+permissions:
+ contents: write
+ pull-requests: write
+
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 2335732c..6e1535aa 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,12 @@
Pixelix is an Android client for [Pixelfed](https://pixelfed.org/), the federated image-sharing social network.
It's designed to provide a seamless and high-performance user experience. With Pixelix, you can easily browse, post, and interact with your Pixelfed network on the go.
+
+
+
+
-
## Please donate
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b2f9b5d8..2b20fed0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,3 +1,4 @@
+import com.google.devtools.ksp.gradle.KspAATask
import org.jetbrains.compose.desktop.application.dsl.TargetFormat.*
plugins {
@@ -93,6 +94,9 @@ kotlin {
//image crop
implementation(libs.krop)
+
+ //video player
+ implementation(libs.composemediaplayer)
}
androidMain.dependencies {
@@ -111,12 +115,8 @@ kotlin {
implementation(libs.material)
//media
- implementation(libs.androidx.media3.exoplayer)
- implementation(libs.androidx.media3.exoplayer.dash)
- implementation(libs.androidx.media3.ui)
- implementation(libs.android.image.cropper)
- implementation(libs.coil.video)
implementation(libs.coil.gif)
+ implementation(libs.coil.video)
// widget
implementation(libs.androidx.glance.appwidget)
@@ -133,11 +133,7 @@ kotlin {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutines.swing)
implementation(libs.ktor.client.okhttp)
- implementation(libs.appdirs)
implementation(libs.slf4j.simple)
- implementation(libs.vlcj)
- implementation(libs.jna)
- implementation(libs.jna.platform)
}
}
@@ -154,8 +150,8 @@ android {
applicationId = "com.daniebeler.pfpixelix"
minSdk = 26
targetSdk = 35
- versionCode = 31
- versionName = "4.1.0"
+ versionCode = 32
+ versionName = "4.1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -183,6 +179,9 @@ android {
isDebuggable = false
isProfileable = false
isShrinkResources = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
+ )
}
}
packaging.resources {
@@ -243,3 +242,8 @@ compose.desktop {
}
}
}
+
+tasks.configureEach {
+ if (this is KspAATask && name != "kspCommonMainKotlinMetadata")
+ dependsOn("kspCommonMainKotlinMetadata")
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 481bb434..16f03358 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,4 +18,5 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+-dontobfuscate
\ No newline at end of file
diff --git a/app/src/androidMain/AndroidManifest.xml b/app/src/androidMain/AndroidManifest.xml
index 6b3791a3..7250b5a3 100644
--- a/app/src/androidMain/AndroidManifest.xml
+++ b/app/src/androidMain/AndroidManifest.xml
@@ -41,6 +41,11 @@
+
+
+
+
+
@@ -64,20 +69,10 @@
-
-
-
-
-
-
-
-
+
+
+
+
{
intent.dataString?.let { onExternalUrl(it) }
}
Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE -> {
- val imageUris = handleSharePhotoIntent(intent, contentResolver, cacheDir)
+ val imageUris = handleSharePhotoIntent(intent, contentResolver, cacheDir, appActivity)
if (imageUris.isNotEmpty()) {
imageUris.forEach { uri ->
try {
@@ -85,7 +81,7 @@ actual fun EdgeToEdgeDialogProperties(
decorFitsSystemWindows = false
)
-private fun saveUriToCache(uri: Uri, contentResolver: ContentResolver, cacheDir: File): Uri? {
+private fun saveUriToCache(uri: Uri, contentResolver: ContentResolver, cacheDir: File, appActivity: AppActivity): Uri? {
try {
val inputStream: InputStream? = contentResolver.openInputStream(uri)
inputStream?.use { input ->
@@ -93,7 +89,11 @@ private fun saveUriToCache(uri: Uri, contentResolver: ContentResolver, cacheDir:
FileOutputStream(file).use { output ->
input.copyTo(output)
}
- return Uri.fromFile(file) // Return the new cached URI
+ return FileProvider.getUriForFile(
+ appActivity,
+ "${appActivity.packageName}.provider",
+ file
+ )
}
} catch (e: Exception) {
e.printStackTrace()
@@ -102,7 +102,7 @@ private fun saveUriToCache(uri: Uri, contentResolver: ContentResolver, cacheDir:
}
private fun handleSharePhotoIntent(
- intent: Intent, contentResolver: ContentResolver, cacheDir: File
+ intent: Intent, contentResolver: ContentResolver, cacheDir: File, appActivity: AppActivity
): List {
val action = intent.action
val type = intent.type
@@ -122,7 +122,7 @@ private fun handleSharePhotoIntent(
) as? Uri
}
singleUri?.let { uri ->
- val cachedUri = saveUriToCache(uri, contentResolver, cacheDir)
+ val cachedUri = saveUriToCache(uri, contentResolver, cacheDir, appActivity)
imageUris =
cachedUri?.let { listOf(it) } ?: emptyList() // Wrap single image in a list
}
@@ -141,7 +141,7 @@ private fun handleSharePhotoIntent(
}
imageUris = receivedUris?.mapNotNull {
saveUriToCache(
- it, contentResolver, cacheDir
+ it, contentResolver, cacheDir, appActivity
)
} ?: emptyList()
}
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt
index f4d84c86..a050e35b 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt
@@ -1,6 +1,5 @@
package com.daniebeler.pfpixelix
-import android.app.Activity
import android.app.Application
import android.content.Context
import androidx.activity.ComponentActivity
@@ -11,7 +10,6 @@ import androidx.work.WorkerParameters
import coil3.SingletonImageLoader
import com.daniebeler.pfpixelix.di.AppComponent
import com.daniebeler.pfpixelix.di.create
-import com.daniebeler.pfpixelix.domain.service.file.AndroidFileService
import com.daniebeler.pfpixelix.domain.service.icon.AndroidAppIconManager
import com.daniebeler.pfpixelix.utils.configureLogger
import com.daniebeler.pfpixelix.widget.notifications.work_manager.LatestImageTask
@@ -28,7 +26,6 @@ class MyApplication : Application(), Configuration.Provider {
override fun onCreate() {
appComponent = AppComponent.create(
this,
- AndroidFileService(this),
AndroidAppIconManager(this)
)
SingletonImageLoader.setSafe {
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/AndroidFileService.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/AndroidFileService.kt
deleted file mode 100644
index 1a31ef1d..00000000
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/AndroidFileService.kt
+++ /dev/null
@@ -1,226 +0,0 @@
-package com.daniebeler.pfpixelix.domain.service.file
-
-import android.content.ContentResolver
-import android.content.ContentValues
-import android.content.Context
-import android.graphics.Bitmap
-import android.net.Uri
-import android.os.Build
-import android.provider.MediaStore
-import android.provider.OpenableColumns
-import android.webkit.MimeTypeMap
-import android.widget.Toast
-import co.touchlab.kermit.Logger
-import coil3.ImageLoader
-import coil3.SingletonImageLoader
-import coil3.request.ImageRequest
-import coil3.request.SuccessResult
-import coil3.request.allowHardware
-import coil3.toBitmap
-import coil3.video.videoFrameMillis
-import com.daniebeler.pfpixelix.utils.KmpContext
-import com.daniebeler.pfpixelix.utils.KmpUri
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlinx.datetime.Clock
-import okio.Path
-import okio.Path.Companion.toPath
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.io.FileNotFoundException
-
-class AndroidFileService(
- private val context: KmpContext
-) : FileService {
- override val dataStoreDir: Path = context.filesDir.path.toPath().resolve("datastore")
- override val imageCacheDir: Path = context.cacheDir.path.toPath().resolve("image_cache")
-
- override fun getFile(uri: KmpUri): PlatformFile? {
- return AndroidFile(uri, context).takeIf { it.isExist() }
- }
-
- override fun downloadFile(name: String?, url: String) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- var uri: Uri? = null
- val saveImageRoutine = CoroutineScope(Dispatchers.Default).launch {
-
- val bitmap: Bitmap? = urlToBitmap(url, context)
- if (bitmap == null) {
- cancel("an error occured when downloading the image")
- return@launch
- }
-
- uri = saveImageToMediaStore(
- context,
- generateUniqueName(name, false, context),
- bitmap
- )
- if (uri == null) {
- cancel("an error occured when saving the image")
- return@launch
- }
- }
-
- saveImageRoutine.invokeOnCompletion { throwable ->
- CoroutineScope(Dispatchers.Main).launch {
- uri?.let {
- Toast.makeText(context, "Stored at: " + uri.toString(), Toast.LENGTH_LONG)
- .show()
- } ?: throwable?.let {
- Toast.makeText(
- context, "an error occurred downloading the image", Toast.LENGTH_LONG
- ).show()
- }
- }
- }
- }
- }
-
- override fun getCacheSizeInBytes(): Long {
- return imageCacheDir.toFile().walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
- }
-
- override fun cleanCache() {
- imageCacheDir.toFile().deleteRecursively()
- }
-
- private fun generateUniqueName(
- imageName: String?, returnFullPath: Boolean, context: KmpContext
- ): String {
-
- val filename = "${imageName}_${Clock.System.now().epochSeconds}"
-
- if (returnFullPath) {
- val directory: File = context.getDir("zest", Context.MODE_PRIVATE)
- return "$directory/$filename"
- } else {
- return filename
- }
- }
-
- private suspend fun urlToBitmap(
- imageURL: String,
- context: KmpContext,
- ): Bitmap? {
- val loader = ImageLoader(context)
- val request = ImageRequest.Builder(context).data(imageURL).allowHardware(false).build()
- val result = loader.execute(request)
- if (result is SuccessResult) {
- return result.image.toBitmap()
- }
- return null
- }
-
- private fun saveImageToMediaStore(
- context: KmpContext,
- displayName: String,
- bitmap: Bitmap
- ): Uri? {
- val imageCollections = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
- } else {
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- }
-
- val imageDetails = ContentValues().apply {
- put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
- put(MediaStore.Images.Media.MIME_TYPE, "image/png")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- put(MediaStore.Images.Media.IS_PENDING, 1)
- }
- }
-
- val resolver = context.applicationContext.contentResolver
- val imageContentUri = resolver.insert(imageCollections, imageDetails) ?: return null
-
- return try {
- resolver.openOutputStream(imageContentUri, "w").use { os ->
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, os!!)
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- imageDetails.clear()
- imageDetails.put(MediaStore.Images.Media.IS_PENDING, 0)
- resolver.update(imageContentUri, imageDetails, null, null)
- }
-
- imageContentUri
- } catch (e: FileNotFoundException) {
- // Some legacy devices won't create directory for the Uri if dir not exist, resulting in
- // a FileNotFoundException. To resolve this issue, we should use the File API to save the
- // image, which allows us to create the directory ourselves.
- null
- }
- }
-}
-
-private class AndroidFile(
- private val uri: Uri,
- private val context: Context
-) : PlatformFile {
- override fun isExist(): Boolean =
- getName() != "AndroidFile:unknown"
-
- override fun getName(): String = when (uri.scheme) {
- ContentResolver.SCHEME_FILE -> uri.pathSegments.last().substringBeforeLast('.')
- ContentResolver.SCHEME_CONTENT -> context.contentResolver.query(
- uri, null, null, null, null
- )?.use {
- val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
- it.moveToFirst()
- it.getString(nameIndex)
- }
-
- else -> null
- } ?: "AndroidFile:unknown"
-
- override fun getSize(): Long = when (uri.scheme) {
- ContentResolver.SCHEME_FILE -> context.contentResolver.openFileDescriptor(uri, "r")
- ?.use { it.statSize }
-
- ContentResolver.SCHEME_CONTENT -> context.contentResolver.query(
- uri, null, null, null, null
- )?.use {
- val nameIndex = it.getColumnIndex(OpenableColumns.SIZE)
- it.moveToFirst()
- it.getLong(nameIndex)
- }
-
- else -> null
- } ?: 0L
-
- override fun getMimeType(): String = when (uri.scheme) {
- ContentResolver.SCHEME_FILE -> {
- val fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
- MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase())
- }
-
- ContentResolver.SCHEME_CONTENT -> {
- context.contentResolver.getType(uri)
- }
-
- else -> null
- } ?: "image/*"
-
- override suspend fun readBytes(): ByteArray = withContext(Dispatchers.IO) {
- context.contentResolver.openInputStream(uri)!!.readBytes()
- }
-
- override suspend fun getThumbnail(): ByteArray? = withContext(Dispatchers.IO) {
- val bm = try {
- val req = ImageRequest.Builder(context).data(uri).videoFrameMillis(0).build()
- val img = SingletonImageLoader.get(context).execute(req)
- img.image?.toBitmap()
- } catch (e: Exception) {
- Logger.e("AndroidFile.getThumbnail error", e)
- null
- } ?: return@withContext null
-
- val stream = ByteArrayOutputStream()
- bm.compress(Bitmap.CompressFormat.PNG, 100, stream)
- stream.toByteArray()
- }
-}
\ No newline at end of file
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AndroidAppIconManager.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AndroidAppIconManager.kt
index 9b8c4c07..5982aac5 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AndroidAppIconManager.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AndroidAppIconManager.kt
@@ -22,7 +22,7 @@ class AndroidAppIconManager(
private val iconIds = mapOf(
Res.drawable.app_icon_00 to "com.daniebeler.pfpixelix.Icon04",
Res.drawable.app_icon_01 to "com.daniebeler.pfpixelix.Icon01",
- Res.drawable.app_icon_02 to "com.daniebeler.pfpixelix.AppActivity",
+ Res.drawable.app_icon_02 to "com.daniebeler.pfpixelix.Icon02",
Res.drawable.app_icon_03 to "com.daniebeler.pfpixelix.Icon03",
Res.drawable.app_icon_05 to "com.daniebeler.pfpixelix.Icon05",
Res.drawable.app_icon_06 to "com.daniebeler.pfpixelix.Icon06",
@@ -45,22 +45,15 @@ class AndroidAppIconManager(
try {
val pm = context.packageManager
for ((res, id) in iconIds.entries) {
- if (res != icon) {
- val i = pm.getComponentEnabledSetting(ComponentName(context, id))
- if (i == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
- pm.setComponentEnabledSetting(
- ComponentName(context, id),
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
- PackageManager.DONT_KILL_APP
- )
- }
- } else {
- pm.setComponentEnabledSetting(
- ComponentName(context, iconIds[icon]!!),
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
- PackageManager.DONT_KILL_APP
- )
- }
+ val state =
+ if (res == icon) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ else PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+
+ pm.setComponentEnabledSetting(
+ ComponentName(context, id),
+ state,
+ PackageManager.DONT_KILL_APP
+ )
}
} catch (e: Error) {
Logger.e("enableCustomIcon", e)
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.android.kt
index f6ee25f4..d104a70f 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.android.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.android.kt
@@ -1,13 +1,10 @@
package com.daniebeler.pfpixelix.domain.service.platform
-import android.os.Build
-
actual object PlatformFeatures {
actual val notificationWidgets = true
actual val inAppBrowser = true
- actual val downloadToGallery = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
+ actual val downloadToGallery = true
actual val customAppIcon = true
- actual val autoplayVideosPref = true
actual val addCollection = true
actual val customAccentColors = false
}
\ 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 3ce9c589..cd476237 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
@@ -1,16 +1,32 @@
package com.daniebeler.pfpixelix.utils
+import android.content.ContentResolver
import android.content.Context
import android.net.Uri
+import android.webkit.MimeTypeMap
import androidx.core.net.toUri
import coil3.PlatformContext
-import io.github.vinceglb.filekit.core.PlatformFile
+import io.github.vinceglb.filekit.PlatformFile
+import io.github.vinceglb.filekit.dialogs.uri
actual typealias KmpUri = Uri
actual val EmptyKmpUri: KmpUri = Uri.EMPTY
actual fun KmpUri.getPlatformUriObject(): Any = this
actual fun String.toKmpUri(): KmpUri = this.toUri()
actual fun PlatformFile.toKmpUri(): KmpUri = this.uri
+actual fun KmpUri.toPlatformFile(): PlatformFile = PlatformFile(this)
actual typealias KmpContext = Context
actual val KmpContext.coilContext: PlatformContext get() = this
+actual fun KmpContext.getMimeType(uri: KmpUri): String = when (uri.scheme) {
+ ContentResolver.SCHEME_FILE -> {
+ val fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
+ MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase())
+ }
+
+ ContentResolver.SCHEME_CONTENT -> {
+ contentResolver.getType(uri)
+ }
+
+ else -> null
+} ?: "image/*"
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.android.kt
deleted file mode 100644
index 86860231..00000000
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.android.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-package com.daniebeler.pfpixelix.utils
-
-import androidx.annotation.OptIn
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.media3.common.AudioAttributes
-import androidx.media3.common.C
-import androidx.media3.common.MediaItem
-import androidx.media3.common.Player
-import androidx.media3.common.Tracks
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.ui.AspectRatioFrameLayout
-import androidx.media3.ui.PlayerView
-import com.daniebeler.pfpixelix.MyApplication
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
-
-actual class VideoPlayer actual constructor(
- context: KmpContext,
- coroutineScope: CoroutineScope
-) {
- actual var progress: ((current: Long, duration: Long) -> Unit)? = null
- actual var hasAudio: ((Boolean) -> Unit)? = null
- actual var isVideoPlaying: ((Boolean) -> Unit)? = null
-
- private val audioAttributes =
- AudioAttributes.Builder()
- .setUsage(C.USAGE_MEDIA)
- .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
- .build()
- private val player = ExoPlayer.Builder(context).build().apply {
- repeatMode = Player.REPEAT_MODE_ONE
- setAudioAttributes(audioAttributes, false)
- addListener(object : Player.Listener {
- @UnstableApi
- override fun onTracksChanged(tracks: Tracks) {
- tracks.groups.forEach { trackGroup ->
- trackGroup.mediaTrackGroup.let { mediaTrackGroup ->
- for (i in 0 until mediaTrackGroup.length) {
- val format = mediaTrackGroup.getFormat(i)
- if (format.sampleMimeType?.startsWith("audio/") == true) {
- hasAudio?.invoke(true)
- break
- }
- }
- }
- }
- }
-
- override fun onIsPlayingChanged(isPlaying: Boolean) {
- isVideoPlaying?.invoke(isPlaying)
- }
- })
- }
-
- init {
- coroutineScope.launch {
- while (isActive) {
- val duration = player.contentDuration
- val currentTime = player.currentPosition
- if (duration > 0 && currentTime <= duration) {
- progress?.invoke(currentTime, duration)
- }
- delay(300)
- }
- }
- }
-
- @OptIn(UnstableApi::class)
- @Composable
- actual fun view(modifier: Modifier) {
- LaunchedEffect(player) {
- player.isPlaying
- }
- AndroidView(
- modifier = modifier,
- factory = { ctx ->
- PlayerView(ctx).apply {
- player = this@VideoPlayer.player
- resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
- setShowPreviousButton(false)
- useController = false
- }
- }
- )
- }
-
- actual fun prepare(url: String) {
- val item = MediaItem.fromUri(url)
- player.setMediaItem(item)
- player.prepare()
- }
-
- actual fun play() {
- player.play()
- }
-
- actual fun pause() {
- player.pause()
- }
-
- actual fun release() {
- player.release()
- }
-
- actual fun audio(enable: Boolean) {
- player.volume = if (enable) 1f else 0f
- player.setAudioAttributes(audioAttributes, enable)
- }
-}
\ No newline at end of file
diff --git a/app/src/androidMain/res/xml/file_paths.xml b/app/src/androidMain/res/xml/file_paths.xml
new file mode 100644
index 00000000..8ad3114d
--- /dev/null
+++ b/app/src/androidMain/res/xml/file_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/commonMain/composeResources/values-en-rUS/strings.xml b/app/src/commonMain/composeResources/values-en-rUS/strings.xml
index 260b83ab..706fd559 100644
--- a/app/src/commonMain/composeResources/values-en-rUS/strings.xml
+++ b/app/src/commonMain/composeResources/values-en-rUS/strings.xml
@@ -66,7 +66,7 @@
yearly
monthly
daily
- I don\'t have a profile
+ I don’t have a profile
Server URL
Are you sure you want to log out?
Logout?
diff --git a/app/src/commonMain/composeResources/values-pl-rPL/strings.xml b/app/src/commonMain/composeResources/values-pl-rPL/strings.xml
index 7345ef66..f08a3434 100644
--- a/app/src/commonMain/composeResources/values-pl-rPL/strings.xml
+++ b/app/src/commonMain/composeResources/values-pl-rPL/strings.xml
@@ -298,5 +298,5 @@
Scam
Terroryzm
Zgłoszono
- Delete Account
+ Usuń konto
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt
index edf6c469..bb1612a5 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt
@@ -2,7 +2,6 @@ package com.daniebeler.pfpixelix
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
@@ -14,6 +13,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.SnackbarHost
+import androidx.compose.material.SnackbarHostState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.UnfoldMore
import androidx.compose.material3.DrawerValue
@@ -27,15 +28,15 @@ import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -54,7 +55,6 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
-import co.touchlab.kermit.Logger
import coil3.compose.AsyncImage
import com.daniebeler.pfpixelix.di.AppComponent
import com.daniebeler.pfpixelix.di.LocalAppComponent
@@ -89,6 +89,10 @@ import pixelix.app.generated.resources.profile
import pixelix.app.generated.resources.search
import pixelix.app.generated.resources.search_outline
+val LocalSnackbarPresenter = compositionLocalOf<(String) -> Unit> {
+ error("No LocalSnackbarPresenter provided")
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun App(
@@ -107,12 +111,6 @@ fun App(
LocalAppComponent provides appComponent
) {
PixelixTheme {
- val navController = rememberNavController()
- val scope = rememberCoroutineScope()
- val drawerState = rememberDrawerState(DrawerValue.Closed)
- val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
- var showAccountSwitchBottomSheet by remember { mutableStateOf(false) }
-
var activeUser by remember { mutableStateOf("unknown") }
LaunchedEffect(Unit) {
val authService = appComponent.authService
@@ -123,86 +121,107 @@ fun App(
}
if (activeUser == "unknown") return@PixelixTheme
- ReverseModalNavigationDrawer(
- gesturesEnabled = drawerState.isOpen,
- drawerState = drawerState,
- drawerContent = {
- ModalDrawerSheet(
- drawerState = drawerState,
- drawerShape = shapes.extraLarge.end(0.dp),
- ) {
- PreferencesComposable(navController, drawerState, {
- scope.launch {
- drawerState.close()
- }
- })
+ key(activeUser) {
+ val scope = rememberCoroutineScope()
+ val drawerState = rememberDrawerState(DrawerValue.Closed)
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ var showAccountSwitchBottomSheet by remember { mutableStateOf(false) }
+ val navController = rememberNavController()
+
+ val snackbarHostState = remember { SnackbarHostState() }
+ val snackBarPresenter: (String) -> Unit = { msg ->
+ scope.launch {
+ snackbarHostState.showSnackbar(msg)
}
}
- ) {
- Scaffold(
- contentWindowInsets = WindowInsets(0),
- bottomBar = {
- BottomBar(
- navController = navController,
- openAccountSwitchBottomSheet = {
- showAccountSwitchBottomSheet = true
+ //Note that wrapping something in key
+ // won't actually clean up any ViewModel instances associated with destinations -
+ // they'll continue to exist and run for the entire lifetime of the containing
+ // Activity/Fragment because you didn't actually destroy them properly,
+ // you just dropped any access to them
+ LaunchedEffect(activeUser) {
+ navController.clearBackStack()
+ navController.clearBackStack()
+ navController.clearBackStack()
+ navController.clearBackStack()
+ navController.clearBackStack()
+ }
+
+ CompositionLocalProvider(
+ LocalSnackbarPresenter provides snackBarPresenter
+ ) {
+ ReverseModalNavigationDrawer(
+ gesturesEnabled = drawerState.isOpen,
+ drawerState = drawerState,
+ drawerContent = {
+ ModalDrawerSheet(
+ drawerState = drawerState,
+ drawerShape = shapes.extraLarge.end(0.dp),
+ ) {
+ PreferencesComposable(navController, drawerState, {
+ scope.launch {
+ drawerState.close()
+ }
+ })
+ }
+ }
+ ) {
+ Scaffold(
+ contentWindowInsets = WindowInsets(0),
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ bottomBar = {
+ BottomBar(
+ navController = navController,
+ openAccountSwitchBottomSheet = {
+ showAccountSwitchBottomSheet = true
+ },
+ )
},
- )
- },
- content = { paddingValues ->
- val startDestination =
- if (activeUser == null) Destination.FirstLogin
- else Destination.HomeTabFeeds
- NavHost(
- modifier = Modifier.fillMaxSize().padding(paddingValues)
- .consumeWindowInsets(WindowInsets.navigationBars),
- navController = navController,
- startDestination = startDestination,
- builder = {
- appGraph(
- navController,
- { scope.launch { drawerState.open() } },
- exitApp
+ content = { paddingValues ->
+ val startDestination =
+ if (activeUser == null) Destination.FirstLogin
+ else Destination.HomeTabFeeds
+ NavHost(
+ modifier = Modifier.fillMaxSize().padding(paddingValues)
+ .consumeWindowInsets(WindowInsets.navigationBars),
+ navController = navController,
+ startDestination = startDestination,
+ builder = {
+ appGraph(
+ navController,
+ { scope.launch { drawerState.open() } },
+ exitApp
+ )
+ }
)
}
)
- val launchUser = remember { activeUser }
- LaunchedEffect(activeUser) {
- if (launchUser == activeUser) return@LaunchedEffect
- val rootScreen =
- if (activeUser == null) Destination.FirstLogin else Destination.HomeTabFeeds
- navController.navigate(rootScreen) {
- val root = navController.currentBackStack.value
- .firstOrNull { it.destination.route != null }
- ?.destination?.route
- if (root != null) {
- popUpTo(root) { inclusive = true }
- }
- }
+ }
+ }
- if (activeUser != null) {
- appComponent.systemFileShare.shareFilesRequests.collect { uris ->
- navController.navigate(
- Destination.NewPost(uris.map { it.toString() })
- )
- }
- }
+ LaunchedEffect(Unit) {
+ appComponent.systemFileShare.shareFilesRequests.collect { uris ->
+ if (activeUser != null) {
+ navController.navigate(
+ Destination.NewPost(uris.map { it.toString() })
+ )
}
}
- )
- }
- if (showAccountSwitchBottomSheet) {
- ModalBottomSheet(
- onDismissRequest = {
- showAccountSwitchBottomSheet = false
- }, sheetState = sheetState
- ) {
- AccountSwitchBottomSheet(
- navController = navController,
- closeBottomSheet = { showAccountSwitchBottomSheet = false },
- null
- )
+ }
+
+ if (showAccountSwitchBottomSheet) {
+ ModalBottomSheet(
+ onDismissRequest = {
+ showAccountSwitchBottomSheet = false
+ }, sheetState = sheetState
+ ) {
+ AccountSwitchBottomSheet(
+ navController = navController,
+ closeBottomSheet = { showAccountSwitchBottomSheet = false },
+ null
+ )
+ }
}
}
}
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 60378d82..12290f09 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt
@@ -15,6 +15,7 @@ import com.daniebeler.pfpixelix.domain.repository.PixelfedApi
import com.daniebeler.pfpixelix.domain.repository.createPixelfedApi
import com.daniebeler.pfpixelix.domain.repository.serializers.SavedSearchesSerializer
import com.daniebeler.pfpixelix.domain.service.file.FileService
+import com.daniebeler.pfpixelix.domain.service.file.toOkIoPath
import com.daniebeler.pfpixelix.domain.service.icon.AppIconManager
import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences
import com.daniebeler.pfpixelix.domain.service.search.SearchFieldFocus
@@ -32,6 +33,8 @@ import com.russhwolf.settings.ExperimentalSettingsImplementation
import com.russhwolf.settings.datastore.DataStoreSettings
import de.jensklingenberg.ktorfit.Ktorfit
import de.jensklingenberg.ktorfit.converter.CallConverterFactory
+import io.github.vinceglb.filekit.resolve
+import io.github.vinceglb.filekit.toKotlinxIoPath
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpSend
import io.ktor.client.plugins.HttpTimeout
@@ -56,7 +59,6 @@ annotation class AppSingleton
@Component
abstract class AppComponent(
@get:Provides val context: KmpContext,
- @get:Provides val fileService: FileService,
@get:Provides val iconManager: AppIconManager,
) {
abstract val systemUrlHandler: SystemUrlHandler
@@ -88,7 +90,7 @@ abstract class AppComponent(
logger = object : io.ktor.client.plugins.logging.Logger {
override fun log(message: String) {
Logger.v("Pixelix HttpClient") {
- message.lines().joinToString { "\n\t\t$it"}
+ message.lines().joinToString { "\n\t\t$it" }
}
}
}
@@ -121,7 +123,9 @@ abstract class AppComponent(
PreferenceDataStoreFactory.createWithPath(
corruptionHandler = null,
migrations = emptyList(),
- produceFile = { fileService.dataStoreDir.resolve("settings.preferences_pb") },
+ produceFile = {
+ FileService.dataStoreDir.resolve("settings.preferences_pb").toOkIoPath()
+ },
)
@Provides
@@ -130,7 +134,9 @@ abstract class AppComponent(
DataStoreFactory.create(
storage = OkioStorage(
fileSystem = FileSystem.SYSTEM,
- producePath = { fileService.dataStoreDir.resolve("saved_searches.json") },
+ producePath = {
+ FileService.dataStoreDir.resolve("saved_searches.json").toOkIoPath()
+ },
serializer = SavedSearchesSerializer,
)
)
@@ -141,7 +147,9 @@ abstract class AppComponent(
DataStoreFactory.create(
storage = OkioStorage(
fileSystem = FileSystem.SYSTEM,
- producePath = { fileService.dataStoreDir.resolve("session_storage_datastore.json") },
+ producePath = {
+ FileService.dataStoreDir.resolve("session_storage_datastore.json").toOkIoPath()
+ },
serializer = SessionStorageDataSerializer,
)
)
@@ -165,7 +173,7 @@ abstract class AppComponent(
.diskCache(
DiskCache.Builder()
.maxSizeBytes(50L * 1024L * 1024L)
- .directory(fileService.imageCacheDir)
+ .directory(FileService.imageCacheDir.toOkIoPath())
.build()
)
.build()
@@ -176,6 +184,5 @@ abstract class AppComponent(
@KmpComponentCreate
expect fun AppComponent.Companion.create(
context: KmpContext,
- fileService: FileService,
iconManager: AppIconManager,
): AppComponent
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/editor/PostEditorService.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/editor/PostEditorService.kt
index d72947f4..d7840aa0 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/editor/PostEditorService.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/editor/PostEditorService.kt
@@ -4,8 +4,15 @@ import com.daniebeler.pfpixelix.domain.model.NewPost
import com.daniebeler.pfpixelix.domain.model.UpdatePost
import com.daniebeler.pfpixelix.domain.repository.PixelfedApi
import com.daniebeler.pfpixelix.domain.service.file.FileService
+import com.daniebeler.pfpixelix.domain.service.file.PlatformFile
import com.daniebeler.pfpixelix.domain.service.utils.loadResource
import com.daniebeler.pfpixelix.utils.KmpUri
+import io.github.vinceglb.filekit.FileKit
+import io.github.vinceglb.filekit.ImageFormat
+import io.github.vinceglb.filekit.compressImage
+import io.github.vinceglb.filekit.exists
+import io.github.vinceglb.filekit.nameWithoutExtension
+import io.github.vinceglb.filekit.readBytes
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.http.Headers
@@ -21,16 +28,26 @@ class PostEditorService(
) {
fun uploadMedia(uri: KmpUri, description: String) = loadResource {
- val file = fileService.getFile(uri) ?: error("File doesn't exist")
+ val file = PlatformFile(uri)
+ if (!file.exists()) error("File doesn't exist")
val bytes = file.readBytes()
- val thumbnail = file.getThumbnail()
+ val mimeType = fileService.getMimeType(file)
+ val thumbnail = if (mimeType.startsWith("image")) {
+ FileKit.compressImage(
+ bytes = bytes,
+ quality = 85,
+ maxWidth = 400,
+ maxHeight = 400,
+ imageFormat = ImageFormat.PNG
+ )
+ } else null
val data = MultiPartFormDataContent(
parts = formData {
append("description", description)
append("file", bytes, Headers.build {
- append(HttpHeaders.ContentType, file.getMimeType())
- append(HttpHeaders.ContentDisposition, "filename=${file.getName()}")
+ append(HttpHeaders.ContentType, mimeType)
+ append(HttpHeaders.ContentDisposition, "filename=${file.nameWithoutExtension}")
})
if (thumbnail != null) {
append("thumbnail", thumbnail, Headers.build {
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileService.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileService.kt
index d08fb0cb..6e88a56e 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileService.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileService.kt
@@ -1,23 +1,82 @@
package com.daniebeler.pfpixelix.domain.service.file
+import co.touchlab.kermit.Logger
+import com.daniebeler.pfpixelix.utils.KmpContext
import com.daniebeler.pfpixelix.utils.KmpUri
+import com.daniebeler.pfpixelix.utils.getMimeType
+import com.daniebeler.pfpixelix.utils.toKmpUri
+import com.daniebeler.pfpixelix.utils.toPlatformFile
+import io.github.vinceglb.filekit.FileKit
+import io.github.vinceglb.filekit.PlatformFile
+import io.github.vinceglb.filekit.cacheDir
+import io.github.vinceglb.filekit.delete
+import io.github.vinceglb.filekit.exists
+import io.github.vinceglb.filekit.filesDir
+import io.github.vinceglb.filekit.isRegularFile
+import io.github.vinceglb.filekit.list
+import io.github.vinceglb.filekit.path
+import io.github.vinceglb.filekit.resolve
+import io.github.vinceglb.filekit.saveImageToGallery
+import io.github.vinceglb.filekit.size
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import io.ktor.client.statement.bodyAsBytes
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
+import me.tatarka.inject.annotations.Inject
import okio.Path
+import okio.Path.Companion.toPath
-interface FileService {
- val dataStoreDir: Path
- val imageCacheDir: Path
+@Inject
+class FileService(
+ private val context: KmpContext,
+ private val httpClient: HttpClient
+) {
+ companion object {
+ val dataStoreDir = FileKit.filesDir.resolve("datastore")
+ val imageCacheDir = FileKit.cacheDir.resolve("image_cache")
+ }
+ private val client = httpClient.config { followRedirects = true }
- fun getFile(uri: KmpUri): PlatformFile?
- fun downloadFile(name: String?, url: String)
- fun getCacheSizeInBytes(): Long
- fun cleanCache()
+ suspend fun getCacheSizeInBytes(): Long = imageCacheDir.sizeRecursively()
+ suspend fun cleanCache() {
+ imageCacheDir.deleteRecursively()
+ }
+
+ suspend fun download(url: String) {
+ with(Dispatchers.IO) {
+ val bytes = client.get(url).bodyAsBytes()
+ val name = url.substringAfterLast('/')
+ Logger.d { "Downloading: $name" }
+ FileKit.saveImageToGallery(bytes, name)
+ }
+ }
+
+ fun getMimeType(file: PlatformFile): String = context.getMimeType(file.toKmpUri())
+
+ private suspend fun PlatformFile.sizeRecursively(): Long {
+ return when {
+ !exists() -> 0L
+ isRegularFile() -> size()
+ else -> list().sumOf { it.sizeRecursively() }
+ }
+ }
+
+ private suspend fun PlatformFile.deleteRecursively() {
+ when {
+ !exists() -> {
+ return
+ }
+ isRegularFile() -> {
+ delete(false)
+ }
+ else -> {
+ list().forEach { it.deleteRecursively() }
+ delete(false)
+ }
+ }
+ }
}
-interface PlatformFile {
- fun isExist(): Boolean
- fun getName(): String
- fun getSize(): Long
- fun getMimeType(): String
- suspend fun readBytes(): ByteArray
- suspend fun getThumbnail(): ByteArray?
-}
\ No newline at end of file
+internal fun PlatformFile(kmpUri: KmpUri): PlatformFile = kmpUri.toPlatformFile()
+internal fun PlatformFile.toOkIoPath(): Path = path.toPath()
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.kt
index 53abb5dd..259b43d8 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.kt
@@ -5,7 +5,6 @@ expect object PlatformFeatures {
val inAppBrowser: Boolean
val downloadToGallery: Boolean
val customAppIcon: Boolean
- val autoplayVideosPref: Boolean
val addCollection: Boolean
val customAccentColors: Boolean
}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_profile/EditProfileComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_profile/EditProfileComposable.kt
index cb4c14be..846fabb5 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_profile/EditProfileComposable.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_profile/EditProfileComposable.kt
@@ -66,9 +66,10 @@ import com.attafitamim.krop.ui.DefaultControls
import com.daniebeler.pfpixelix.EdgeToEdgeDialogProperties
import com.daniebeler.pfpixelix.di.injectViewModel
import com.daniebeler.pfpixelix.utils.imeAwareInsets
-import io.github.vinceglb.filekit.compose.rememberFilePickerLauncher
-import io.github.vinceglb.filekit.core.PickerMode
-import io.github.vinceglb.filekit.core.PickerType
+import io.github.vinceglb.filekit.dialogs.FileKitMode
+import io.github.vinceglb.filekit.dialogs.FileKitType
+import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher
+import io.github.vinceglb.filekit.readBytes
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
@@ -182,7 +183,7 @@ fun EditProfileComposable(
}
val filePicker = rememberFilePickerLauncher(
- type = PickerType.Image, mode = PickerMode.Single
+ type = FileKitType.Image, mode = FileKitMode.Single
) { file ->
file ?: return@rememberFilePickerLauncher
coroutineScope.launch {
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 29e2d839..34043e74 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
@@ -79,9 +79,9 @@ import com.daniebeler.pfpixelix.utils.KmpUri
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 io.github.vinceglb.filekit.dialogs.FileKitMode
+import io.github.vinceglb.filekit.dialogs.FileKitType
+import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
@@ -314,7 +314,7 @@ fun ImagesPager(
Spacer(Modifier.height(48.dp))
val launcher = rememberFilePickerLauncher(
- type = PickerType.ImageAndVideo, mode = PickerMode.Multiple()
+ type = FileKitType.ImageAndVideo, mode = FileKitMode.Multiple()
) { files ->
files?.forEach { file ->
addImage(file.toKmpUri())
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostViewModel.kt
index e8679d61..56c5cba3 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostViewModel.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostViewModel.kt
@@ -12,11 +12,13 @@ import com.daniebeler.pfpixelix.domain.model.NewPost
import com.daniebeler.pfpixelix.domain.model.Visibility
import com.daniebeler.pfpixelix.domain.service.editor.PostEditorService
import com.daniebeler.pfpixelix.domain.service.file.FileService
+import com.daniebeler.pfpixelix.domain.service.file.PlatformFile
import com.daniebeler.pfpixelix.domain.service.instance.InstanceService
import com.daniebeler.pfpixelix.domain.service.utils.Resource
import com.daniebeler.pfpixelix.ui.navigation.Destination
-import com.daniebeler.pfpixelix.utils.EmptyKmpUri
import com.daniebeler.pfpixelix.utils.KmpUri
+import io.github.vinceglb.filekit.exists
+import io.github.vinceglb.filekit.size
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.flowOn
@@ -96,8 +98,9 @@ class NewPostViewModel @Inject constructor(
}
fun addImage(uri: KmpUri) {
- val file = fileService.getFile(uri) ?: return
- val fileType = file.getMimeType()
+ val file = PlatformFile(uri)
+ if (!file.exists()) return
+ val fileType = fileService.getMimeType(file)
if (instance != null && !instance!!.configuration.mediaAttachmentConfig.supportedMimeTypes.contains(
fileType
)
@@ -108,7 +111,7 @@ class NewPostViewModel @Inject constructor(
)
return
}
- val size = file.getSize()
+ val size = file.size()
if (fileType.take(5) == "image") {
if (instance != null && size > instance!!.configuration.mediaAttachmentConfig.imageSizeLimit) {
@@ -284,8 +287,13 @@ class NewPostViewModel @Inject constructor(
postEditorService.createPost(createPostDto).onEach { result ->
createPostState = when (result) {
is Resource.Success -> {
- navController.navigate(Destination.OwnProfile)
- CreatePostState(post = result.data, isLoading = true)
+ navController.navigate(Destination.HomeTabOwnProfile) {
+ restoreState = false
+ popUpTo {
+ inclusive = true
+ }
+ }
+ CreatePostState()
}
is Resource.Error -> {
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/notifications/CustomNotification.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/notifications/CustomNotification.kt
index 710639eb..2682f648 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/notifications/CustomNotification.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/notifications/CustomNotification.kt
@@ -171,10 +171,10 @@ fun CustomNotification(
navController.navigate(
Destination.Post(
id = if (doesMediaAttachmentExsist) {
- notification.account.id
+ notification.post!!.id
} else {
viewModel.ancestor!!.id
- }, openReplies = true
+ }, openReplies = !doesMediaAttachmentExsist
)
)
})
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostComposable.kt
index 2bbb9827..0812eb7a 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostComposable.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostComposable.kt
@@ -617,7 +617,8 @@ fun PostComposable(
viewModel,
post,
pagerState.currentPage,
- navController
+ navController,
+ { showBottomSheet = 0 }
)
} else {
ShareBottomSheet(
@@ -626,7 +627,8 @@ fun PostComposable(
viewModel,
post,
pagerState.currentPage,
- navController
+ navController,
+ { showBottomSheet = 0 }
)
}
} else if (showBottomSheet == 3) {
@@ -729,7 +731,7 @@ fun PostImage(
showMediaDialog = mediaAttachment
})
}) {
- if (mediaAttachment.type == "image") {
+ if (mediaAttachment.type != "video") {
ImageWrapper(
mediaAttachment,
{ zoomState.setContentSize(it.painter.intrinsicSize) },
@@ -846,7 +848,7 @@ fun MediaDialog(
contentAlignment = Alignment.Center
) {
Box(modifier = Modifier.zIndex(2f).zoomable(zoomState).clickable { }) {
- if (mediaAttachment.type == "image") {
+ if (mediaAttachment.type != "video") {
ImageWrapper(
mediaAttachment,
{ zoomState.setContentSize(it.painter.intrinsicSize) },
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostViewModel.kt
index ce250f83..f1f3e2d1 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostViewModel.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostViewModel.kt
@@ -433,8 +433,10 @@ class PostViewModel @Inject constructor(
platform.openUrl(url)
}
- fun saveImage(name: String?, url: String) {
- fileService.downloadFile(name, url)
+ fun saveImage(url: String) {
+ viewModelScope.launch {
+ fileService.download(url)
+ }
}
fun shareText(text: String) {
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ShareBottomSheet.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ShareBottomSheet.kt
index 20aacb7f..47434c46 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ShareBottomSheet.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ShareBottomSheet.kt
@@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
+import com.daniebeler.pfpixelix.LocalSnackbarPresenter
import com.daniebeler.pfpixelix.domain.model.MediaAttachment
import com.daniebeler.pfpixelix.domain.model.Post
import com.daniebeler.pfpixelix.domain.model.Visibility
@@ -42,13 +43,13 @@ import pixelix.app.generated.resources.license
import pixelix.app.generated.resources.open_in_browser
import pixelix.app.generated.resources.open_outline
import pixelix.app.generated.resources.pencil_outline
+import pixelix.app.generated.resources.report_this_post
import pixelix.app.generated.resources.share_social_outline
import pixelix.app.generated.resources.share_this_post
import pixelix.app.generated.resources.trash_outline
import pixelix.app.generated.resources.unlisted
import pixelix.app.generated.resources.visibility_x
import pixelix.app.generated.resources.warning
-import pixelix.app.generated.resources.report_this_post
@Composable
fun ShareBottomSheet(
@@ -57,7 +58,8 @@ fun ShareBottomSheet(
viewModel: PostViewModel,
post: Post,
currentMediaAttachmentNumber: Int,
- navController: NavController
+ navController: NavController,
+ closeBottomSheet: () -> Unit
) {
var humanReadableVisibility by remember {
@@ -106,6 +108,7 @@ fun ShareBottomSheet(
Res.string.license, mediaAttachment.license.title
), onClick = {
viewModel.openUrl(mediaAttachment.license.url)
+ closeBottomSheet()
})
}
@@ -116,6 +119,7 @@ fun ShareBottomSheet(
Res.string.open_in_browser
), onClick = {
viewModel.openUrl(url)
+ closeBottomSheet()
})
ButtonRowElement(
@@ -123,19 +127,23 @@ fun ShareBottomSheet(
text = stringResource(Res.string.share_this_post),
onClick = {
viewModel.shareText(url)
+ closeBottomSheet()
})
- if (mediaAttachment != null && PlatformFeatures.downloadToGallery && mediaAttachment.type == "image") {
+ if (
+ PlatformFeatures.downloadToGallery &&
+ mediaAttachment?.url != null
+ ) {
+ val snackbarPresenter = LocalSnackbarPresenter.current
ButtonRowElement(
icon = Res.drawable.cloud_download_outline,
text = stringResource(Res.string.download_image),
onClick = {
-
- viewModel.saveImage(
- post.account.username,
- viewModel.post!!.mediaAttachments[currentMediaAttachmentNumber].url!!
- )
- })
+ viewModel.saveImage(mediaAttachment.url)
+ snackbarPresenter("Image saved to the gallery")
+ closeBottomSheet()
+ }
+ )
}
if (minePost) {
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/VideoAttachment.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/VideoAttachment.kt
index 7b6bbf7b..74f20175 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/VideoAttachment.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/VideoAttachment.kt
@@ -19,10 +19,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -30,9 +28,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
-import com.daniebeler.pfpixelix.di.LocalAppComponent
import com.daniebeler.pfpixelix.domain.model.MediaAttachment
-import com.daniebeler.pfpixelix.utils.VideoPlayer
+import io.github.kdroidfilter.composemediaplayer.VideoPlayerSurface
+import io.github.kdroidfilter.composemediaplayer.rememberVideoPlayerState
@Composable
fun VideoAttachment(
@@ -40,24 +38,26 @@ fun VideoAttachment(
viewModel: PostViewModel,
onReady: () -> Unit
) {
- val coroutineScope = rememberCoroutineScope()
- val context = LocalAppComponent.current.context
- val player = remember { VideoPlayer(context, coroutineScope) }
- var progress by remember { mutableFloatStateOf(0f) }
- var hasAudio by remember { mutableStateOf(false) }
- var isPlaying by remember { mutableStateOf(false) }
+ val player = rememberVideoPlayerState().apply {
+ loop = true
+ userDragging = false
+ }
+ LaunchedEffect(attachment) {
+ player.openUri(attachment.url.orEmpty())
+ }
var videoFrameIsVisible by remember { mutableStateOf(false) }
Column {
Box(Modifier.clickable {
- if (isPlaying) {
+ if (player.isPlaying) {
player.pause()
} else {
player.play()
}
}) {
- player.view(
+ VideoPlayerSurface(
+ playerState = player,
modifier = Modifier
.fillMaxWidth()
.run {
@@ -66,6 +66,7 @@ fun VideoAttachment(
}
.isVisible(threshold = 50) { videoFrameIsVisible = it }
)
+ val hasAudio = (player.metadata.audioChannels ?: 0) > 0
if (hasAudio) {
IconButton(
modifier = Modifier
@@ -93,38 +94,22 @@ fun VideoAttachment(
}
}
LinearProgressIndicator(
- progress = { progress },
+ progress = { player.sliderPos / 1000 },
modifier = Modifier.fillMaxWidth(),
trackColor = MaterialTheme.colorScheme.background
)
}
- LaunchedEffect(attachment) {
- player.prepare(attachment.url.orEmpty())
- }
- val started = progress > 0
- LaunchedEffect(started) { onReady() }
+ val started = player.sliderPos > 0
+ LaunchedEffect(started) { if (started) onReady() }
LaunchedEffect(viewModel.volume) {
- player.audio(viewModel.volume)
- }
-
- DisposableEffect(Unit) {
- player.progress = { current, duration ->
- progress = current.toFloat() / duration.toFloat()
- }
- player.hasAudio = { hasAudio = it }
- player.isVideoPlaying = { isPlaying = it }
-
- onDispose {
- player.progress = null
- player.hasAudio = null
- player.release()
- }
+ player.volume = if (viewModel.volume) 1f else 0f
}
- LaunchedEffect(videoFrameIsVisible) {
- if (videoFrameIsVisible && viewModel.isAutoplayVideos) {
+ val autoPlay = videoFrameIsVisible && viewModel.isAutoplayVideos
+ LaunchedEffect(autoPlay) {
+ if (autoPlay) {
player.play()
} else {
player.pause()
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginViewModel.kt
index a0c15234..3a196db9 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginViewModel.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginViewModel.kt
@@ -6,18 +6,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import androidx.lifecycle.viewmodel.compose.viewModel
import com.daniebeler.pfpixelix.domain.model.Server
import com.daniebeler.pfpixelix.domain.service.instance.InstanceService
import com.daniebeler.pfpixelix.domain.service.platform.Platform
import com.daniebeler.pfpixelix.domain.service.session.AuthService
import com.daniebeler.pfpixelix.domain.service.utils.Resource
-import com.daniebeler.pfpixelix.ui.composables.settings.about_instance.InstanceState
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject
-import pixelix.app.generated.resources.Res
@Inject
class LoginViewModel(
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/icon_selection/IconSelectionComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/icon_selection/IconSelectionComposable.kt
index 280fde85..e9e3ea4b 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/icon_selection/IconSelectionComposable.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/icon_selection/IconSelectionComposable.kt
@@ -96,17 +96,6 @@ fun IconSelectionComposable(
state = lazyGridState,
columns = GridCells.Fixed(3)
) {
- item(span = { GridItemSpan(3) }) {
- Column {
- Row {
- Text(text = stringResource(Res.string.two_icons_info))
- }
-
- HorizontalDivider(Modifier.padding(vertical = 12.dp))
- }
-
- }
-
items(viewModel.icons) { icon ->
Image(
painterResource(icon),
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/PreferencesComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/PreferencesComposable.kt
index 8db93f34..ab1083e2 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/PreferencesComposable.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/PreferencesComposable.kt
@@ -97,9 +97,7 @@ fun PreferencesComposable(
UseInAppBrowserPref()
}
- if (PlatformFeatures.autoplayVideosPref) {
- AutoplayVideoPref()
- }
+ AutoplayVideoPref()
RepostSettingsPref { viewModel.openRepostSettings() }
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ClearCachePref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ClearCachePref.kt
index 6e484bd4..7494e946 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ClearCachePref.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ClearCachePref.kt
@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.daniebeler.pfpixelix.di.injectViewModel
import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.SettingPref
import org.jetbrains.compose.resources.stringResource
@@ -15,10 +16,10 @@ import pixelix.app.generated.resources.save_outline
@Composable
fun ClearCachePref(drawerState: DrawerState) {
val viewModel = injectViewModel("ClearCacheViewModel") { clearCacheViewModel }
- val cacheSize = remember { mutableStateOf("") }
+ val cacheSize = viewModel.cacheSize.collectAsStateWithLifecycle("")
LaunchedEffect(drawerState.isOpen) {
- cacheSize.value = humanReadableByteCountSI(viewModel.getCacheSizeInBytes())
+ viewModel.refresh()
}
SettingPref(
@@ -26,25 +27,6 @@ fun ClearCachePref(drawerState: DrawerState) {
title = stringResource(Res.string.clear_cache),
desc = cacheSize.value,
trailingContent = null,
- onClick = {
- viewModel.cleanCache()
- cacheSize.value = humanReadableByteCountSI(viewModel.getCacheSizeInBytes())
- }
+ onClick = { viewModel.cleanCache() }
)
}
-
-private fun humanReadableByteCountSI(bytes: Long): String {
- var bytes = bytes
- if (-1000 < bytes && bytes < 1000) {
- return "$bytes B"
- }
- val chars = "kMGTPE".toCharArray()
- var ci = 0
- while (bytes <= -999950 || bytes >= 999950) {
- bytes /= 1000
- ci++
- }
-
- val valueRounded = (bytes / 100.0).toInt() / 10.0 // Round down to one decimal place
- return "$valueRounded ${chars[ci]}B"
-}
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ClearCacheViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ClearCacheViewModel.kt
index 24320948..9fb22ff2 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ClearCacheViewModel.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ClearCacheViewModel.kt
@@ -1,15 +1,53 @@
package com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.prefs
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import com.daniebeler.pfpixelix.domain.service.file.FileService
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject
@Inject
class ClearCacheViewModel(
private val fileService: FileService
-): ViewModel() {
- fun getCacheSizeInBytes() = fileService.getCacheSizeInBytes()
+) : ViewModel() {
+
+ private val cacheSizeState = MutableStateFlow(0L)
+ val cacheSize = cacheSizeState.onStart {
+ cacheSizeState.value = fileService.getCacheSizeInBytes()
+ }.map { bytes ->
+ humanReadableByteCountSI(bytes)
+ }
+
+ fun refresh() {
+ viewModelScope.launch {
+ cacheSizeState.value = fileService.getCacheSizeInBytes()
+ }
+ }
+
fun cleanCache() {
- fileService.cleanCache()
+ viewModelScope.launch {
+ fileService.cleanCache()
+ cacheSizeState.value = fileService.getCacheSizeInBytes()
+ }
+ }
+
+ private fun humanReadableByteCountSI(bytes: Long): String {
+ var bytes = bytes
+ if (-1000 < bytes && bytes < 1000) {
+ return "$bytes B"
+ }
+ val chars = "kMGTPE".toCharArray()
+ var ci = 0
+ while (bytes <= -999950 || bytes >= 999950) {
+ bytes /= 1000
+ ci++
+ }
+
+ val valueRounded = (bytes / 100.0).toInt() / 10.0 // Round down to one decimal place
+ return "$valueRounded ${chars[ci]}B"
}
}
\ 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 f6a4e070..59021680 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt
@@ -1,7 +1,7 @@
package com.daniebeler.pfpixelix.utils
import coil3.PlatformContext
-import io.github.vinceglb.filekit.core.PlatformFile
+import io.github.vinceglb.filekit.PlatformFile
expect abstract class KmpUri {
abstract override fun toString(): String
@@ -10,7 +10,9 @@ expect val EmptyKmpUri: KmpUri
expect fun KmpUri.getPlatformUriObject(): Any
expect fun String.toKmpUri(): KmpUri
expect fun PlatformFile.toKmpUri(): KmpUri
+expect fun KmpUri.toPlatformFile(): PlatformFile
expect abstract class KmpContext
expect val KmpContext.coilContext: PlatformContext
+expect fun KmpContext.getMimeType(uri: KmpUri): String
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.kt
deleted file mode 100644
index 588d321d..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.daniebeler.pfpixelix.utils
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import kotlinx.coroutines.CoroutineScope
-
-expect class VideoPlayer(
- context: KmpContext,
- coroutineScope: CoroutineScope
-) {
- var progress: ((current: Long, duration: Long) -> Unit)?
- var hasAudio: ((Boolean) -> Unit)?
- var isVideoPlaying: ((Boolean) -> Unit)?
-
- @Composable
- fun view(modifier: Modifier)
-
- fun prepare(url: String)
- fun play()
- fun pause()
- fun release()
- fun audio(enable: Boolean)
-}
\ 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 9b796e37..30eb5481 100644
--- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt
+++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt
@@ -7,7 +7,6 @@ import androidx.compose.ui.window.ComposeUIViewController
import coil3.SingletonImageLoader
import com.daniebeler.pfpixelix.di.AppComponent
import com.daniebeler.pfpixelix.di.create
-import com.daniebeler.pfpixelix.domain.service.file.IosFileService
import com.daniebeler.pfpixelix.domain.service.icon.IosAppIconManager
import com.daniebeler.pfpixelix.utils.KmpContext
import com.daniebeler.pfpixelix.utils.configureLogger
@@ -27,7 +26,6 @@ fun AppViewController(urlCallback: IosUrlCallback): UIViewController {
object : KmpContext() {
override val viewController get() = viewController!!
},
- IosFileService(),
IosAppIconManager()
)
diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/IosFileService.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/IosFileService.kt
deleted file mode 100644
index 6b7fda13..00000000
--- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/IosFileService.kt
+++ /dev/null
@@ -1,175 +0,0 @@
-package com.daniebeler.pfpixelix.domain.service.file
-
-import com.daniebeler.pfpixelix.utils.KmpUri
-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 okio.Path
-import okio.Path.Companion.toPath
-import platform.CoreFoundation.CFDataGetBytePtr
-import platform.CoreFoundation.CFDataGetLength
-import platform.CoreFoundation.CFDictionaryAddValue
-import platform.CoreFoundation.CFDictionaryCreateMutable
-import platform.CoreFoundation.CFRelease
-import platform.CoreFoundation.CFStringRef
-import platform.CoreFoundation.CFURLRef
-import platform.CoreGraphics.CGDataProviderCopyData
-import platform.CoreGraphics.CGImageGetDataProvider
-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.NSData
-import platform.Foundation.NSDictionary
-import platform.Foundation.NSDocumentDirectory
-import platform.Foundation.NSFileManager
-import platform.Foundation.NSFileSize
-import platform.Foundation.NSNumber
-import platform.Foundation.NSString
-import platform.Foundation.NSUserDomainMask
-import platform.Foundation.dataWithContentsOfURL
-import platform.Foundation.fileSize
-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)
-class IosFileService : FileService {
- private fun appDocDir() = NSFileManager.defaultManager.URLForDirectory(
- directory = NSDocumentDirectory,
- inDomain = NSUserDomainMask,
- appropriateForURL = null,
- create = false,
- error = null,
- )!!.path!!.toPath()
-
- override val dataStoreDir: Path = appDocDir().resolve("dataStore")
- override val imageCacheDir: Path = appDocDir().resolve("imageCache")
-
-
- override fun getFile(uri: KmpUri): PlatformFile? {
- return IosFile(uri).takeIf { it.isExist() }
- }
-
- override fun downloadFile(name: String?, url: String) {
- }
-
- override fun getCacheSizeInBytes(): Long {
- val fm = NSFileManager.defaultManager()
- val files = fm.subpathsOfDirectoryAtPath(imageCacheDir.toString(), null).orEmpty()
- var result = 0uL
- files.map { file ->
- val dict = fm.fileAttributesAtPath(
- imageCacheDir.resolve(file.toString()).toString(),
- true
- ) as NSDictionary
- result += dict.fileSize()
- }
- return result.toLong()
- }
-
- override fun cleanCache() {
- val fm = NSFileManager.defaultManager()
- fm.removeItemAtPath(imageCacheDir.toString(), null)
- }
-}
-
-@OptIn(ExperimentalForeignApi::class)
-private class IosFile(
- private val uri: KmpUri
-) : PlatformFile {
- override fun isExist(): Boolean =
- getName() != "IosFile:unknown"
-
- override fun getName(): String {
- return uri.url.lastPathComponent() ?: "IosFile:unknown"
- }
-
- override fun getSize(): Long {
- val path = uri.url.path ?: return 0L
- val fm = NSFileManager.defaultManager
- val attr = fm.attributesOfItemAtPath(path, null) ?: return 0L
- return attr.getValue(NSFileSize) as Long
- }
-
- override fun getMimeType(): String {
- val fileExtension = uri.url.pathExtension()
- @Suppress("UNCHECKED_CAST", "CAST_NEVER_SUCCEEDS")
- 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
- }
-
- override suspend fun readBytes(): ByteArray = withContext(Dispatchers.IO) {
- val data = NSData.dataWithContentsOfURL(uri.url)!!
- ByteArray(data.length.toInt()).apply {
- data.usePinned {
- memcpy(refTo(0), data.bytes, data.length)
- }
- }
- }
-
- override suspend fun getThumbnail(): ByteArray? = withContext(Dispatchers.IO) {
- @Suppress("UNCHECKED_CAST")
- 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/domain/service/platform/PlatformFeatures.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.ios.kt
index d44c2ceb..fb16777e 100644
--- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.ios.kt
+++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.ios.kt
@@ -3,9 +3,8 @@ package com.daniebeler.pfpixelix.domain.service.platform
actual object PlatformFeatures {
actual val notificationWidgets = false
actual val inAppBrowser = true
- actual val downloadToGallery = false
+ actual val downloadToGallery = false //https://github.com/vinceglb/FileKit/issues/215
actual val customAppIcon = true
- actual val autoplayVideosPref = false
actual val addCollection = false
actual val customAccentColors = true
}
\ 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 a17bae93..7018ec51 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
@@ -1,7 +1,17 @@
package com.daniebeler.pfpixelix.utils
import coil3.PlatformContext
-import io.github.vinceglb.filekit.core.PlatformFile
+import io.github.vinceglb.filekit.PlatformFile
+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
import platform.Foundation.NSURL
import platform.UIKit.UIViewController
@@ -17,8 +27,25 @@ actual val EmptyKmpUri: KmpUri = IosUri(NSURL(string = ""))
actual fun KmpUri.getPlatformUriObject(): Any = url
actual fun String.toKmpUri(): KmpUri = IosUri(NSURL(string = this))
actual fun PlatformFile.toKmpUri(): KmpUri = IosUri(nsUrl)
+actual fun KmpUri.toPlatformFile(): PlatformFile = PlatformFile(url)
actual abstract class KmpContext {
abstract val viewController: UIViewController
}
actual val KmpContext.coilContext get() = PlatformContext.INSTANCE
+
+@OptIn(ExperimentalForeignApi::class)
+actual fun KmpContext.getMimeType(uri: KmpUri): String {
+ val fileExtension = uri.url.pathExtension()
+ @Suppress("UNCHECKED_CAST", "CAST_NEVER_SUCCEEDS")
+ 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
+}
diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.ios.kt
deleted file mode 100644
index 4d667e5b..00000000
--- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.ios.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-package com.daniebeler.pfpixelix.utils
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.viewinterop.UIKitView
-import kotlinx.cinterop.ExperimentalForeignApi
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import platform.AVFoundation.AVLayerVideoGravityResize
-import platform.AVFoundation.AVMediaTypeAudio
-import platform.AVFoundation.AVPlayer
-import platform.AVFoundation.AVPlayerItem
-import platform.AVFoundation.AVPlayerLayer
-import platform.AVFoundation.asset
-import platform.AVFoundation.currentItem
-import platform.AVFoundation.currentTime
-import platform.AVFoundation.duration
-import platform.AVFoundation.muted
-import platform.AVFoundation.pause
-import platform.AVFoundation.play
-import platform.AVFoundation.replaceCurrentItemWithPlayerItem
-import platform.AVFoundation.tracksWithMediaType
-import platform.AVKit.AVPlayerViewController
-import platform.CoreMedia.CMTimeGetSeconds
-import platform.Foundation.NSURL
-import platform.Foundation.observeValueForKeyPath
-import platform.UIKit.UIView
-
-@OptIn(ExperimentalForeignApi::class)
-actual class VideoPlayer actual constructor(
- context: KmpContext,
- private val coroutineScope: CoroutineScope
-) {
- actual var progress: ((current: Long, duration: Long) -> Unit)? = null
- actual var hasAudio: ((Boolean) -> Unit)? = null
- actual var isVideoPlaying: ((Boolean) -> Unit)? = null
-
- private var extractAudioInfoJob: Job? = null
-
- private val player = AVPlayer()
- private val playerViewController = AVPlayerViewController().apply {
- this.player = this@VideoPlayer.player
- this.videoGravity = AVLayerVideoGravityResize
- this.showsPlaybackControls = false
- this.view.userInteractionEnabled = false
- }
- private val playerLayer = AVPlayerLayer().apply {
- this.player = playerViewController.player
- }
-
- init {
- coroutineScope.launch {
- while (isActive) {
- player.currentItem?.let {
- val duration = CMTimeGetSeconds(it.duration()).toLong()
- val currentTime = CMTimeGetSeconds(it.currentTime()).toLong()
- if (duration > 0 && currentTime <= duration) {
- progress?.invoke(currentTime, duration)
- }
- }
- delay(300)
- }
- }
- }
-
- @Composable
- actual fun view(modifier: Modifier) {
- UIKitView(
- modifier = modifier,
- factory = {
- UIView().apply {
- playerLayer.setFrame(frame)
- playerViewController.view.setFrame(frame)
- addSubview(playerViewController.view)
- }
- },
- update = { view: UIView ->
- playerLayer.setFrame(view.frame)
- playerViewController.view.setFrame(view.frame)
- }
- )
- }
-
- actual fun prepare(url: String) {
- release()
- val item = AVPlayerItem(NSURL(string = url))
- player.replaceCurrentItemWithPlayerItem(item)
-
- extractAudioInfoJob = coroutineScope.launch {
- val withAudio = withContext(Dispatchers.Default) {
- item.asset.tracksWithMediaType(AVMediaTypeAudio).isNotEmpty()
- }
- hasAudio?.invoke(withAudio)
- }
- }
-
- actual fun play() {
- player.play()
- }
- actual fun pause() {
- player.pause()
- }
- actual fun release() {
- player.replaceCurrentItemWithPlayerItem(null)
- extractAudioInfoJob?.cancel()
- }
- actual fun audio(enable: Boolean) {
- player.muted = !enable
- }
-}
\ No newline at end of file
diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt
index ffc09aca..a647aa85 100644
--- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt
+++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt
@@ -1,33 +1,31 @@
package com.daniebeler.pfpixelix
+import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import coil3.SingletonImageLoader
import com.daniebeler.pfpixelix.di.AppComponent
import com.daniebeler.pfpixelix.di.create
-import com.daniebeler.pfpixelix.domain.service.file.DesktopFileService
import com.daniebeler.pfpixelix.domain.service.icon.DesktopAppIconManager
import com.daniebeler.pfpixelix.utils.KmpContext
import com.daniebeler.pfpixelix.utils.configureJavaLogger
-import com.daniebeler.pfpixelix.utils.configureLogger
+import io.github.vinceglb.filekit.FileKit
import java.awt.Desktop
import java.awt.Dimension
fun main() {
- //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-swing-interoperability.html
- System.setProperty("compose.swing.render.on.graphics", "true")
- System.setProperty("compose.interop.blending", "true")
application {
+ FileKit.init("com.daniebeler.pfpixelix")
+ configureJavaLogger()
+
val appComponent = AppComponent.Companion.create(
object : KmpContext() {},
- DesktopFileService(),
DesktopAppIconManager()
)
- configureJavaLogger()
-
SingletonImageLoader.setSafe {
appComponent.provideImageLoader()
}
@@ -40,7 +38,11 @@ fun main() {
Window(
title = "Pixelix",
- state = rememberWindowState(width = 600.dp, height = 1000.dp),
+ state = rememberWindowState(
+ width = 400.dp,
+ height = 800.dp,
+ position = WindowPosition.Aligned(Alignment.Center)
+ ),
onCloseRequest = ::exitApplication,
) {
window.minimumSize = Dimension(400, 600)
diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/DesktopFileService.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/DesktopFileService.kt
deleted file mode 100644
index 580bd0ae..00000000
--- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/DesktopFileService.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.daniebeler.pfpixelix.domain.service.file
-
-import ca.gosyer.appdirs.AppDirs
-import co.touchlab.kermit.Logger
-import com.daniebeler.pfpixelix.utils.KmpUri
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import okio.Path
-import okio.Path.Companion.toPath
-import java.awt.Image
-import java.awt.image.BufferedImage
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.nio.file.Files
-import javax.imageio.ImageIO
-
-class DesktopFileService : FileService {
- private val appDirs = AppDirs("com.daniebeler.pfpixelix", null)
- private fun appDocDir() = appDirs.getUserDataDir().toPath()
-
- override val dataStoreDir: Path = appDocDir().resolve("dataStore")
- override val imageCacheDir: Path = appDocDir().resolve("imageCache")
-
- override fun getFile(uri: KmpUri): PlatformFile? {
- return DesktopFile(uri).takeIf { it.isExist() }
- }
-
- override fun downloadFile(name: String?, url: String) {
- }
-
- override fun getCacheSizeInBytes(): Long {
- return imageCacheDir.toFile().walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
- }
-
- override fun cleanCache() {
- imageCacheDir.toFile().deleteRecursively()
- }
-}
-
-private class DesktopFile(
- uri: KmpUri
-) : PlatformFile {
- private val file = File(uri.uri)
-
- override fun isExist(): Boolean = file.exists()
- override fun getName(): String = file.name
- override fun getSize(): Long = file.length()
- override fun getMimeType(): String = Files.probeContentType(file.toPath())
-
- override suspend fun readBytes(): ByteArray = withContext(Dispatchers.IO) {
- file.readBytes()
- }
-
- override suspend fun getThumbnail(): ByteArray? = withContext(Dispatchers.IO) {
- val thumbnail = try {
- val size = 512
- val originalImage = ImageIO.read(file)
- val aspectRatio = originalImage.width.toDouble() / originalImage.height
- val (width, height) = if (aspectRatio > 1) {
- size to (size / aspectRatio).toInt()
- } else {
- (size * aspectRatio).toInt() to size
- }
- val image = originalImage.getScaledInstance(width, height, Image.SCALE_SMOOTH)
- val bufferedImage = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
- val graphics = bufferedImage.createGraphics()
- graphics.drawImage(image, 0, 0, null)
- graphics.dispose()
- bufferedImage
- } catch (e: Exception) {
- Logger.e("Failed to create thumbnail for file: ${file.name}", e)
- null
- } ?: return@withContext null
-
- val outputStream = ByteArrayOutputStream()
- ImageIO.write(thumbnail, "png", outputStream)
- outputStream.toByteArray()
- }
-}
\ No newline at end of file
diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.jvm.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.jvm.kt
index a44074ef..bc794239 100644
--- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.jvm.kt
+++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.jvm.kt
@@ -3,9 +3,8 @@ package com.daniebeler.pfpixelix.domain.service.platform
actual object PlatformFeatures {
actual val notificationWidgets = false
actual val inAppBrowser = false
- actual val downloadToGallery = false
+ actual val downloadToGallery = true
actual val customAppIcon = false
- actual val autoplayVideosPref = false
actual val addCollection = true
actual val customAccentColors = true
}
\ No newline at end of file
diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.jvm.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.jvm.kt
index c945d542..d085a1ce 100644
--- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.jvm.kt
+++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.jvm.kt
@@ -1,8 +1,11 @@
package com.daniebeler.pfpixelix.utils
import coil3.PlatformContext
-import io.github.vinceglb.filekit.core.PlatformFile
+import io.github.vinceglb.filekit.PlatformFile
+import java.io.File
import java.net.URI
+import java.nio.file.Files
+import kotlin.io.path.toPath
private data class DesktopUri(override val uri: URI) : KmpUri() {
override fun toString(): String = uri.toString()
@@ -16,6 +19,8 @@ actual val EmptyKmpUri: KmpUri = DesktopUri(URI(""))
actual fun KmpUri.getPlatformUriObject(): Any = uri.toString()
actual fun String.toKmpUri(): KmpUri = DesktopUri(URI(this))
actual fun PlatformFile.toKmpUri(): KmpUri = DesktopUri(file.toURI())
+actual fun KmpUri.toPlatformFile(): PlatformFile = PlatformFile(File(uri))
actual abstract class KmpContext
actual val KmpContext.coilContext get() = PlatformContext.INSTANCE
+actual fun KmpContext.getMimeType(uri: KmpUri): String = Files.probeContentType(uri.uri.toPath())
diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.jvm.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.jvm.kt
deleted file mode 100644
index f46fd721..00000000
--- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.jvm.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-package com.daniebeler.pfpixelix.utils
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.awt.SwingPanel
-import androidx.compose.ui.graphics.Color
-import kotlinx.coroutines.CoroutineScope
-import uk.co.caprica.vlcj.factory.discovery.NativeDiscovery
-import uk.co.caprica.vlcj.factory.discovery.strategy.OsxNativeDiscoveryStrategy
-import uk.co.caprica.vlcj.player.base.MediaPlayer
-import uk.co.caprica.vlcj.player.base.MediaPlayerEventAdapter
-import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
-import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent
-import uk.co.caprica.vlcj.player.component.InputEvents
-import java.awt.Component
-import java.util.Locale
-
-actual class VideoPlayer actual constructor(
- context: KmpContext,
- private val coroutineScope: CoroutineScope
-) {
- private val mpComponent = initializeMediaPlayerComponent()
- private val player = mpComponent.mediaPlayer()
- actual var isVideoPlaying: ((Boolean) -> Unit)? = null
-
- actual var progress: ((current: Long, duration: Long) -> Unit)? = null
- actual var hasAudio: ((Boolean) -> Unit)? = null
-
- private val listener = object : MediaPlayerEventAdapter() {
- override fun mediaPlayerReady(mediaPlayer: MediaPlayer?) {
- hasAudio?.invoke(player.audio().trackCount() > 0)
- }
-
- override fun positionChanged(mediaPlayer: MediaPlayer?, newPosition: Float) {
- val status = player.status()
- progress?.invoke((status.length() * status.position()).toLong(), status.length())
- }
-
- override fun playing(mediaPlayer: MediaPlayer?) {
- isVideoPlaying?.invoke(true)
- }
-
- override fun paused(mediaPlayer: MediaPlayer?) {
- isVideoPlaying?.invoke(false)
- }
- }
-
- init {
- player.events().addMediaPlayerEventListener(listener)
- }
-
- @Composable
- actual fun view(modifier: Modifier) {
- SwingPanel(
- factory = { mpComponent },
- background = Color.Transparent,
- modifier = modifier
- )
- }
-
- actual fun prepare(url: String) {
- player.media().prepare(url)
- }
-
- actual fun play() {
- player.controls().play()
- }
-
- actual fun pause() {
- player.controls().pause()
- }
-
- actual fun release() {
- player.events().removeMediaPlayerEventListener(listener)
- player.release()
- }
-
- actual fun audio(enable: Boolean) {
- player.audio().isMute = !enable
- }
-
- private fun Component.mediaPlayer() = when (this) {
- is CallbackMediaPlayerComponent -> mediaPlayer()
- is EmbeddedMediaPlayerComponent -> mediaPlayer()
- else -> error("mediaPlayer() can only be called on vlcj player components")
- }
-
- private fun initializeMediaPlayerComponent(): Component {
- NativeDiscovery(OsxNativeDiscoveryStrategy()).discover()
- return if (isMacOS()) {
- CallbackMediaPlayerComponent(null, null, InputEvents.NONE, true, null)
- } else {
- EmbeddedMediaPlayerComponent(null, null, null, InputEvents.NONE, null)
- }
- }
-
- private fun isMacOS(): Boolean {
- val os = System.getProperty("os.name", "generic").lowercase(Locale.ENGLISH)
- return "mac" in os || "darwin" in os
- }
-}
\ No newline at end of file
diff --git a/appstorebadgewhite.svg b/appstorebadgewhite.svg
new file mode 100644
index 00000000..16c0496c
--- /dev/null
+++ b/appstorebadgewhite.svg
@@ -0,0 +1,46 @@
+
diff --git a/gradle.properties b/gradle.properties
index 413a6066..f906e1c8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
#Gradle
-org.gradle.jvmargs=-Xmx8G
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=1G
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.daemon=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3022f950..b8203b79 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,49 +1,45 @@
[versions]
-kotlin = "2.1.20"
-ksp = "2.1.20-1.0.31"
-agp = "8.9.1"
+kotlin = "2.1.21"
+ksp = "2.1.21-2.0.1"
+agp = "8.10.0"
#https://github.com/JetBrains/compose-multiplatform/releases
-composeMultiplatform = "1.8.0-beta02"
-lifecycleMultiplatform = "2.9.0-alpha06"
-navigationMultiplatform = "2.9.0-alpha16"
+composeMultiplatform = "1.8.0"
+lifecycleMultiplatform = "2.9.0-beta01"
+navigationMultiplatform = "2.9.0-beta01"
#JetBrains
-kotlinx-coroutines = "1.10.1"
-kotlinxCollectionsImmutable = "0.3.8"
-kotlinxSerializationJson = "1.8.0"
-ktor = "3.1.1"
+kotlinx-coroutines = "1.10.2"
+kotlinxCollectionsImmutable = "0.4.0"
+kotlinxSerializationJson = "1.8.1"
+ktor = "3.1.3"
kotlinx-datetime = "0.6.2"
#multiplatform
-ksoup = "0.2.2"
+ksoup = "0.2.3"
kermit = "2.0.5"
-ktorfit = "2.4.1"
-kotlinInject = "0.7.2"
+ktorfit = "2.5.2"
+kotlinInject = "0.8.0"
androidx-annotation = "1.9.1"
-coil = "3.1.0"
-datastorePreferences = "1.1.4"
+coil = "3.2.0"
+datastorePreferences = "1.1.6"
multiplatformSettings = "1.3.0"
-filekitCompose = "0.8.8"
-krop = "0.2.0-alpha01"
+filekitCompose = "0.10.0-beta04"
+krop = "0.2.0"
+composemediaplayer = "0.7.4"
#android
accompanistSystemuicontroller = "0.36.0"
activityCompose = "1.10.1"
-androidImageCropper = "4.6.0"
browser = "1.8.0"
coreKtx = "1.16.0"
glance = "1.1.1"
material = "1.12.0"
-media3 = "1.6.0"
-okio = "3.10.2"
-workRuntimeKtx = "2.10.0"
+okio = "3.11.0"
+workRuntimeKtx = "2.10.1"
#desktop
-appdirs = "1.2.0"
slf4jSimple = "2.0.17"
-vlcj = "4.10.1"
-jna = "5.17.0"
[libraries]
@@ -57,7 +53,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
compose-ui-graphics = { module = "org.jetbrains.compose.ui:ui-graphics", version.ref = "composeMultiplatform" }
androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleMultiplatform" }
-androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleMultiplatform" }
+androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleMultiplatform" }
androidx-lifecycle-viewmodel-savedstate = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycleMultiplatform" }
androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationMultiplatform" }
@@ -85,19 +81,12 @@ coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
ktorfit = { module = "de.jensklingenberg.ktorfit:ktorfit-lib", version.ref = "ktorfit" }
ktorfit-call = { module = "de.jensklingenberg.ktorfit:ktorfit-converters-call", version.ref = "ktorfit" }
-jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
-jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
-
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
-android-image-cropper = { module = "com.vanniktech:android-image-cropper", version.ref = "androidImageCropper" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-browser = { module = "androidx.browser:browser", version.ref = "browser" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" }
androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" }
-androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
-androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "media3" }
-androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
material = { module = "com.google.android.material:material", version.ref = "material" }
ksoup = { module = "com.fleeksoft.ksoup:ksoup", version.ref = "ksoup" }
@@ -106,11 +95,10 @@ multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", vers
multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatformSettings" }
multiplatform-settings-datastore = { module = "com.russhwolf:multiplatform-settings-datastore", version.ref = "multiplatformSettings" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
-filekit-compose = { module = "io.github.vinceglb:filekit-compose", version.ref = "filekitCompose" }
+filekit-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekitCompose" }
krop = { module = "com.attafitamim.krop:ui", version.ref = "krop" }
-appdirs = { module = "ca.gosyer:kotlin-multiplatform-appdirs", version.ref = "appdirs" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4jSimple" }
-vlcj = { module = "uk.co.caprica:vlcj", version.ref = "vlcj" }
+composemediaplayer = { module = "io.github.kdroidfilter:composemediaplayer", version.ref = "composemediaplayer" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 33918ab3..2b55fa47 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Mon Dec 11 12:09:16 CET 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
index 9921c142..3a06fde5 100644
--- a/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -296,7 +296,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = 4FA7X6639Y;
+ DEVELOPMENT_TEAM = 6KS4K8Z64D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
@@ -308,7 +308,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1;
- PRODUCT_BUNDLE_IDENTIFIER = com.daniebeler.pfpixelix.iosApp;
+ PRODUCT_BUNDLE_IDENTIFIER = com.terrakok.pfpixelix.iosApp;
PRODUCT_NAME = Pixelix;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@@ -326,7 +326,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = 4FA7X6639Y;
+ DEVELOPMENT_TEAM = 6KS4K8Z64D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
@@ -338,7 +338,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.1;
- PRODUCT_BUNDLE_IDENTIFIER = com.daniebeler.pfpixelix.iosApp;
+ PRODUCT_BUNDLE_IDENTIFIER = com.terrakok.pfpixelix.iosApp;
PRODUCT_NAME = Pixelix;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
diff --git a/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme b/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme
new file mode 100644
index 00000000..c44e9e2d
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/metadata/de/changelogs/32.txt b/metadata/de/changelogs/32.txt
new file mode 100644
index 00000000..d16ce97b
--- /dev/null
+++ b/metadata/de/changelogs/32.txt
@@ -0,0 +1,4 @@
+- Bild teilen gefixt
+- Benutzerdefiniertes App Icon gefixt
+- Benachrichtigungen gefixt
+- Weitere Bugs gefixt
\ No newline at end of file
diff --git a/metadata/en-US/changelogs/32.txt b/metadata/en-US/changelogs/32.txt
new file mode 100644
index 00000000..a99875b0
--- /dev/null
+++ b/metadata/en-US/changelogs/32.txt
@@ -0,0 +1,4 @@
+- Fix image sharing
+- Fix custom App icon
+- Fix notifications
+- Other bug fixes
\ No newline at end of file