From e106815a73cc3f49942ce713bb9cf27e6c47eddb Mon Sep 17 00:00:00 2001 From: Hiebeler Date: Thu, 10 Apr 2025 20:06:49 +0200 Subject: [PATCH 01/30] switch log level to all again --- .../kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..54263a3f 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt @@ -92,7 +92,7 @@ abstract class AppComponent( } } } - level = LogLevel.NONE + level = LogLevel.ALL } install(HttpTimeout) { requestTimeoutMillis = 60000 From b982d4ad2b08d81a98231b054d878c26c51649b3 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 11 Apr 2025 10:45:14 +0200 Subject: [PATCH 02/30] Migrate to FileKit 0.10.0 version and implement common download feature. --- app/build.gradle.kts | 1 - .../domain/service/file/AndroidFileService.kt | 111 +----------------- .../platform/PlatformFeatures.android.kt | 4 +- .../pfpixelix/utils/KmpPlatform.android.kt | 3 +- .../domain/service/file/FileDownloader.kt | 32 +++++ .../domain/service/file/FileService.kt | 18 +-- .../edit_profile/EditProfileComposable.kt | 9 +- .../composables/newpost/NewPostComposable.kt | 8 +- .../ui/composables/post/PostComposable.kt | 6 +- .../ui/composables/post/PostViewModel.kt | 9 +- .../ui/composables/post/ShareBottomSheet.kt | 28 +++-- .../daniebeler/pfpixelix/utils/KmpPlatform.kt | 2 +- .../domain/service/file/IosFileService.kt | 21 +--- .../service/platform/PlatformFeatures.ios.kt | 2 +- .../pfpixelix/utils/KmpPlatform.ios.kt | 2 +- .../kotlin/com/daniebeler/pfpixelix/Main.kt | 9 +- .../domain/service/file/DesktopFileService.kt | 16 +-- .../service/platform/PlatformFeatures.jvm.kt | 2 +- .../pfpixelix/utils/KmpPlatform.jvm.kt | 2 +- gradle/libs.versions.toml | 8 +- iosApp/iosApp.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/xcschemes/iosApp.xcscheme | 78 ++++++++++++ 22 files changed, 190 insertions(+), 189 deletions(-) create mode 100644 app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileDownloader.kt create mode 100644 iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b2f9b5d8..8b6286b2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -133,7 +133,6 @@ 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) 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 index 1a31ef1d..811a7ff5 100644 --- 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 @@ -34,51 +34,11 @@ 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") - +) : FileService() { 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() } } @@ -86,75 +46,6 @@ class AndroidFileService( 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( 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..6a8cf22c 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,11 +1,9 @@ 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 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..bece3050 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 @@ -4,7 +4,8 @@ import android.content.Context import android.net.Uri 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 diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileDownloader.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileDownloader.kt new file mode 100644 index 00000000..6fc7292c --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileDownloader.kt @@ -0,0 +1,32 @@ +package com.daniebeler.pfpixelix.domain.service.file + +import co.touchlab.kermit.Logger +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.write +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.onDownload +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsBytes +import io.ktor.client.statement.readBytes +import io.ktor.client.statement.readRawBytes +import io.ktor.utils.io.reader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.tatarka.inject.annotations.Inject + +@Inject +class FileDownloader(httpClient: HttpClient) { + private val client = httpClient.config { followRedirects = true } + fun download(file: PlatformFile, url: String) { + GlobalScope.launch(Dispatchers.IO) { + Logger.d { "Downloading: $url -> $file" } + val bytes = client.get(url).bodyAsBytes() + file.write(bytes) + } + } +} \ No newline at end of file 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..b4f13603 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,16 +1,20 @@ package com.daniebeler.pfpixelix.domain.service.file import com.daniebeler.pfpixelix.utils.KmpUri +import io.github.vinceglb.filekit.FileKit +import io.github.vinceglb.filekit.cacheDir +import io.github.vinceglb.filekit.filesDir +import io.github.vinceglb.filekit.path import okio.Path +import okio.Path.Companion.toPath -interface FileService { - val dataStoreDir: Path - val imageCacheDir: Path +abstract class FileService { + val dataStoreDir: Path = FileKit.filesDir.path.toPath().resolve("datastore") + val imageCacheDir: Path = FileKit.cacheDir.path.toPath().resolve("image_cache") - fun getFile(uri: KmpUri): PlatformFile? - fun downloadFile(name: String?, url: String) - fun getCacheSizeInBytes(): Long - fun cleanCache() + abstract fun getFile(uri: KmpUri): PlatformFile? + abstract fun getCacheSizeInBytes(): Long + abstract fun cleanCache() } interface PlatformFile { 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/post/PostComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostComposable.kt index 2bbb9827..f3baecac 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) { 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..fcf3f7c8 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 @@ -12,7 +12,7 @@ import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.domain.model.ReportObjectType import com.daniebeler.pfpixelix.domain.service.account.AccountService import com.daniebeler.pfpixelix.domain.service.editor.PostEditorService -import com.daniebeler.pfpixelix.domain.service.file.FileService +import com.daniebeler.pfpixelix.domain.service.file.FileDownloader import com.daniebeler.pfpixelix.domain.service.platform.Platform import com.daniebeler.pfpixelix.domain.service.post.PostService import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences @@ -20,6 +20,7 @@ import com.daniebeler.pfpixelix.domain.service.session.AuthService import com.daniebeler.pfpixelix.domain.service.utils.Resource import com.daniebeler.pfpixelix.ui.composables.post.reply.OwnReplyState import com.daniebeler.pfpixelix.ui.composables.post.reply.RepliesState +import io.github.vinceglb.filekit.PlatformFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -35,7 +36,7 @@ class PostViewModel @Inject constructor( private val authService: AuthService, private val accountService: AccountService, private val platform: Platform, - private val fileService: FileService + private val fileDownloader: FileDownloader ) : ViewModel() { var post: Post? by mutableStateOf(null) @@ -433,8 +434,8 @@ class PostViewModel @Inject constructor( platform.openUrl(url) } - fun saveImage(name: String?, url: String) { - fileService.downloadFile(name, url) + fun saveImage(file: PlatformFile, url: String) { + fileDownloader.download(file, 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..162839d6 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 @@ -26,6 +26,7 @@ import com.daniebeler.pfpixelix.domain.model.Visibility import com.daniebeler.pfpixelix.domain.service.platform.PlatformFeatures import com.daniebeler.pfpixelix.ui.composables.ButtonRowElement import com.daniebeler.pfpixelix.ui.navigation.Destination +import io.github.vinceglb.filekit.dialogs.compose.rememberFileSaverLauncher import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource @@ -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,29 @@ 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 fileSaverLauncher = rememberFileSaverLauncher { file -> + if (file != null) { + viewModel.saveImage(file, mediaAttachment.url) + } + closeBottomSheet() + } ButtonRowElement( icon = Res.drawable.cloud_download_outline, text = stringResource(Res.string.download_image), onClick = { - - viewModel.saveImage( - post.account.username, - viewModel.post!!.mediaAttachments[currentMediaAttachmentNumber].url!! + fileSaverLauncher.launch( + suggestedName = post.account.username + "_" + mediaAttachment.id, + extension = mediaAttachment.url.substringAfterLast('.') ) - }) + } + ) } if (minePost) { 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..38bc08b2 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 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 index 6b7fda13..69eddab2 100644 --- 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 @@ -1,6 +1,10 @@ package com.daniebeler.pfpixelix.domain.service.file import com.daniebeler.pfpixelix.utils.KmpUri +import io.github.vinceglb.filekit.FileKit +import io.github.vinceglb.filekit.cacheDir +import io.github.vinceglb.filekit.filesDir +import io.github.vinceglb.filekit.path import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.get import kotlinx.cinterop.refTo @@ -43,26 +47,11 @@ 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") - - +class IosFileService : FileService() { 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() 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..d19c2eed 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,7 +3,7 @@ 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 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..c15d6aa3 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,7 @@ package com.daniebeler.pfpixelix.utils import coil3.PlatformContext -import io.github.vinceglb.filekit.core.PlatformFile +import io.github.vinceglb.filekit.PlatformFile import platform.Foundation.NSURL import platform.UIKit.UIViewController diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt index ffc09aca..b4d14f8d 100644 --- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt @@ -12,22 +12,21 @@ 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() } 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 index 580bd0ae..83236d75 100644 --- 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 @@ -1,8 +1,11 @@ package com.daniebeler.pfpixelix.domain.service.file -import ca.gosyer.appdirs.AppDirs import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.utils.KmpUri +import io.github.vinceglb.filekit.FileKit +import io.github.vinceglb.filekit.cacheDir +import io.github.vinceglb.filekit.filesDir +import io.github.vinceglb.filekit.path import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okio.Path @@ -14,20 +17,11 @@ 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") - +class DesktopFileService : FileService() { 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() } } 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..29bf7755 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,7 +3,7 @@ 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 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..29f6984b 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,7 +1,7 @@ package com.daniebeler.pfpixelix.utils import coil3.PlatformContext -import io.github.vinceglb.filekit.core.PlatformFile +import io.github.vinceglb.filekit.PlatformFile import java.net.URI private data class DesktopUri(override val uri: URI) : KmpUri() { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3022f950..e4ba19dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ lifecycleMultiplatform = "2.9.0-alpha06" navigationMultiplatform = "2.9.0-alpha16" #JetBrains -kotlinx-coroutines = "1.10.1" +kotlinx-coroutines = "1.10.2" kotlinxCollectionsImmutable = "0.3.8" kotlinxSerializationJson = "1.8.0" ktor = "3.1.1" @@ -24,7 +24,7 @@ androidx-annotation = "1.9.1" coil = "3.1.0" datastorePreferences = "1.1.4" multiplatformSettings = "1.3.0" -filekitCompose = "0.8.8" +filekitCompose = "0.10.0-beta01" krop = "0.2.0-alpha01" #android @@ -40,7 +40,6 @@ okio = "3.10.2" workRuntimeKtx = "2.10.0" #desktop -appdirs = "1.2.0" slf4jSimple = "2.0.17" vlcj = "4.10.1" jna = "5.17.0" @@ -106,9 +105,8 @@ 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" } 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 83444ab11e1f4f0dc563cab797806ac3970156a6 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sun, 13 Apr 2025 23:01:44 +0200 Subject: [PATCH 03/30] Fix navigation problems when user is changed - reset tab states on user change - drop launch user id state --- .../kotlin/com/daniebeler/pfpixelix/App.kt | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt index edf6c469..d72809b2 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt @@ -167,16 +167,33 @@ fun App( ) } ) - val launchUser = remember { activeUser } + var launchUser by remember { mutableStateOf(activeUser) } LaunchedEffect(activeUser) { - if (launchUser == activeUser) return@LaunchedEffect - val rootScreen = - if (activeUser == null) Destination.FirstLogin else Destination.HomeTabFeeds - navController.navigate(rootScreen) { + if (launchUser == activeUser) { + //start destination is already correct. + //we don't need to open a new screen + return@LaunchedEffect + } + launchUser = activeUser + + Logger.d { "Switch user: $activeUser" } + navController.apply { + //clear saved tab's states + clearBackStack() + clearBackStack() + clearBackStack() + clearBackStack() + clearBackStack() val root = navController.currentBackStack.value - .firstOrNull { it.destination.route != null } - ?.destination?.route - if (root != null) { + .firstNotNullOf { it.destination.route } + + val rootScreen = + if (activeUser == null) Destination.FirstLogin + else Destination.HomeTabFeeds + + Logger.d { "Drop the root: $root" } + Logger.d { "And open a new root screen: $rootScreen" } + navigate(rootScreen) { popUpTo(root) { inclusive = true } } } From c47ec11a8264842304e2bde96c57d02899ba619f Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sun, 13 Apr 2025 23:36:38 +0200 Subject: [PATCH 04/30] Fix android app icon customization. Now there should be only one icon in the launcher, --- app/src/androidMain/AndroidManifest.xml | 2 +- .../service/icon/AndroidAppIconManager.kt | 27 +++++++------------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/app/src/androidMain/AndroidManifest.xml b/app/src/androidMain/AndroidManifest.xml index 6b3791a3..ffe1f3a1 100644 --- a/app/src/androidMain/AndroidManifest.xml +++ b/app/src/androidMain/AndroidManifest.xml @@ -77,7 +77,7 @@ Date: Tue, 15 Apr 2025 10:23:16 +0200 Subject: [PATCH 05/30] Simplify user switch logic. --- .../kotlin/com/daniebeler/pfpixelix/App.kt | 168 ++++++++---------- 1 file changed, 71 insertions(+), 97 deletions(-) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt index d72809b2..525a28b8 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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 @@ -107,12 +108,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,104 +118,83 @@ 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() - Scaffold( - contentWindowInsets = WindowInsets(0), - 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 - ) - } - ) - var launchUser by remember { mutableStateOf(activeUser) } - LaunchedEffect(activeUser) { - if (launchUser == activeUser) { - //start destination is already correct. - //we don't need to open a new screen - return@LaunchedEffect - } - launchUser = activeUser - - Logger.d { "Switch user: $activeUser" } - navController.apply { - //clear saved tab's states - clearBackStack() - clearBackStack() - clearBackStack() - clearBackStack() - clearBackStack() - val root = navController.currentBackStack.value - .firstNotNullOf { it.destination.route } - - val rootScreen = - if (activeUser == null) Destination.FirstLogin - else Destination.HomeTabFeeds - - Logger.d { "Drop the root: $root" } - Logger.d { "And open a new root screen: $rootScreen" } - navigate(rootScreen) { - popUpTo(root) { inclusive = true } - } - } - - if (activeUser != null) { - appComponent.systemFileShare.shareFilesRequests.collect { uris -> - navController.navigate( - Destination.NewPost(uris.map { it.toString() }) - ) + ReverseModalNavigationDrawer( + gesturesEnabled = drawerState.isOpen, + drawerState = drawerState, + drawerContent = { + ModalDrawerSheet( + drawerState = drawerState, + drawerShape = shapes.extraLarge.end(0.dp), + ) { + PreferencesComposable(navController, drawerState, { + scope.launch { + drawerState.close() } - } + }) } } - ) - } - if (showAccountSwitchBottomSheet) { - ModalBottomSheet( - onDismissRequest = { - showAccountSwitchBottomSheet = false - }, sheetState = sheetState ) { - AccountSwitchBottomSheet( - navController = navController, - closeBottomSheet = { showAccountSwitchBottomSheet = false }, - null + Scaffold( + contentWindowInsets = WindowInsets(0), + 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 + ) + } + ) + } ) } + + 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 + ) + } + } } } } From ba41453c05a59faf02c9c832a58484ef63d9bead Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Tue, 15 Apr 2025 10:58:21 +0200 Subject: [PATCH 06/30] Fix double app icons in open redirects --- app/src/androidMain/AndroidManifest.xml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/src/androidMain/AndroidManifest.xml b/app/src/androidMain/AndroidManifest.xml index ffe1f3a1..e3330838 100644 --- a/app/src/androidMain/AndroidManifest.xml +++ b/app/src/androidMain/AndroidManifest.xml @@ -64,16 +64,6 @@ - - - - - - - - Date: Tue, 15 Apr 2025 09:37:24 +0000 Subject: [PATCH 07/30] New Crowdin translations by GitHub Action --- app/src/commonMain/composeResources/values-pl-rPL/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 96a83085bf8a8d7aabeada2b76845c58c4bbaa52 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 18 Apr 2025 15:14:32 +0200 Subject: [PATCH 08/30] Migrate to ComposeMediaPlayer --- app/build.gradle.kts | 12 +- .../domain/service/file/AndroidFileService.kt | 15 --- .../pfpixelix/utils/VideoPlayer.android.kt | 115 ------------------ .../ui/composables/post/VideoAttachment.kt | 98 ++++++--------- .../prefs/PreferencesComposable.kt | 4 +- .../daniebeler/pfpixelix/utils/VideoPlayer.kt | 23 ---- .../daniebeler/pfpixelix/AppViewController.kt | 2 +- .../pfpixelix/utils/VideoPlayer.ios.kt | 115 ------------------ .../kotlin/com/daniebeler/pfpixelix/Main.kt | 8 +- .../pfpixelix/utils/VideoPlayer.jvm.kt | 101 --------------- gradle/libs.versions.toml | 30 ++--- settings.gradle.kts | 2 + 12 files changed, 65 insertions(+), 460 deletions(-) delete mode 100644 app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.android.kt delete mode 100644 app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.kt delete mode 100644 app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.ios.kt delete mode 100644 app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.jvm.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8b6286b2..1a3fda0b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -93,6 +93,9 @@ kotlin { //image crop implementation(libs.krop) + + //video player + implementation(libs.composemediaplayer) } androidMain.dependencies { @@ -111,12 +114,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) @@ -134,9 +133,6 @@ kotlin { implementation(libs.kotlinx.coroutines.swing) implementation(libs.ktor.client.okhttp) implementation(libs.slf4j.simple) - implementation(libs.vlcj) - implementation(libs.jna) - implementation(libs.jna.platform) } } 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 index 811a7ff5..9edc4952 100644 --- 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 @@ -1,36 +1,21 @@ 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 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/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..8160a4c4 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,65 +66,47 @@ fun VideoAttachment( } .isVisible(threshold = 50) { videoFrameIsVisible = it } ) - if (hasAudio) { - IconButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(8.dp), - onClick = { - viewModel.toggleVolume(!viewModel.volume) - }, - colors = IconButtonDefaults.filledTonalIconButtonColors() - ) { - if (viewModel.volume) { - Icon( - Icons.AutoMirrored.Outlined.VolumeUp, - contentDescription = "Volume on", - Modifier.size(18.dp) - ) - } else { - Icon( - Icons.AutoMirrored.Outlined.VolumeOff, - contentDescription = "Volume off", - Modifier.size(18.dp) - ) - } + IconButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(8.dp), + onClick = { + viewModel.toggleVolume(!viewModel.volume) + }, + colors = IconButtonDefaults.filledTonalIconButtonColors() + ) { + if (viewModel.volume) { + Icon( + Icons.AutoMirrored.Outlined.VolumeUp, + contentDescription = "Volume on", + Modifier.size(18.dp) + ) + } else { + Icon( + Icons.AutoMirrored.Outlined.VolumeOff, + contentDescription = "Volume off", + Modifier.size(18.dp) + ) } } } 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/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/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..62ff362a 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt @@ -31,7 +31,7 @@ fun AppViewController(urlCallback: IosUrlCallback): UIViewController { IosAppIconManager() ) - configureLogger() + configureLogger(true) SingletonImageLoader.setSafe { appComponent.provideImageLoader() 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 b4d14f8d..20a75c75 100644 --- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt @@ -1,7 +1,9 @@ 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 @@ -39,7 +41,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/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/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4ba19dd..e4b019c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,45 +4,42 @@ ksp = "2.1.20-1.0.31" agp = "8.9.1" #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.10+dev2370" +lifecycleMultiplatform = "2.9.10+dev2370" +navigationMultiplatform = "2.9.10+dev2370" #JetBrains kotlinx-coroutines = "1.10.2" kotlinxCollectionsImmutable = "0.3.8" -kotlinxSerializationJson = "1.8.0" -ktor = "3.1.1" +kotlinxSerializationJson = "1.8.1" +ktor = "3.1.2" kotlinx-datetime = "0.6.2" #multiplatform ksoup = "0.2.2" kermit = "2.0.5" -ktorfit = "2.4.1" +ktorfit = "2.5.0" kotlinInject = "0.7.2" androidx-annotation = "1.9.1" coil = "3.1.0" datastorePreferences = "1.1.4" multiplatformSettings = "1.3.0" filekitCompose = "0.10.0-beta01" -krop = "0.2.0-alpha01" +krop = "0.2.0-alpha02" +composemediaplayer = "0.6.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" +okio = "3.11.0" workRuntimeKtx = "2.10.0" #desktop slf4jSimple = "2.0.17" -vlcj = "4.10.1" -jna = "5.17.0" [libraries] @@ -84,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" } @@ -108,7 +98,7 @@ okio = { module = "com.squareup.okio:okio", version.ref = "okio" } filekit-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekitCompose" } krop = { module = "com.attafitamim.krop:ui", version.ref = "krop" } 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/settings.gradle.kts b/settings.gradle.kts index 53ccade0..8f3d6c23 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ pluginManagement { google() mavenCentral() gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } dependencyResolutionManagement { @@ -10,6 +11,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } From dcfe1f1232007d987a88acbfa78905bd76d3d0cf Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 18 Apr 2025 17:05:10 +0200 Subject: [PATCH 09/30] Refactor: File management and cache handling - Replaced platform-specific file services with a unified FileService using FileKit. - Implemented cache size calculation and clearing within the FileService. - Updated ClearCacheViewModel to use the new FileService and display cache size in a human-readable format. - Integrated file operations into various parts of the application, such as image upload and display. --- .../com/daniebeler/pfpixelix/MyApplication.kt | 3 - .../domain/service/file/AndroidFileService.kt | 102 ----------- .../pfpixelix/utils/KmpPlatform.android.kt | 15 ++ .../daniebeler/pfpixelix/di/AppComponent.kt | 21 ++- .../service/editor/PostEditorService.kt | 25 ++- .../domain/service/file/FileService.kt | 65 +++++-- .../composables/newpost/NewPostViewModel.kt | 11 +- .../ui/composables/session/LoginViewModel.kt | 3 - .../preferences/prefs/prefs/ClearCachePref.kt | 26 +-- .../prefs/prefs/ClearCacheViewModel.kt | 44 ++++- .../daniebeler/pfpixelix/utils/KmpPlatform.kt | 2 + .../daniebeler/pfpixelix/AppViewController.kt | 4 +- .../domain/service/file/IosFileService.kt | 164 ------------------ .../pfpixelix/utils/KmpPlatform.ios.kt | 27 +++ .../kotlin/com/daniebeler/pfpixelix/Main.kt | 3 - .../domain/service/file/DesktopFileService.kt | 73 -------- .../pfpixelix/utils/KmpPlatform.jvm.kt | 5 + gradle/libs.versions.toml | 2 +- 18 files changed, 189 insertions(+), 406 deletions(-) delete mode 100644 app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/AndroidFileService.kt delete mode 100644 app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/IosFileService.kt delete mode 100644 app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/DesktopFileService.kt 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 9edc4952..00000000 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/AndroidFileService.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.daniebeler.pfpixelix.domain.service.file - -import android.content.ContentResolver -import android.content.Context -import android.graphics.Bitmap -import android.net.Uri -import android.provider.OpenableColumns -import android.webkit.MimeTypeMap -import co.touchlab.kermit.Logger -import coil3.SingletonImageLoader -import coil3.request.ImageRequest -import coil3.toBitmap -import coil3.video.videoFrameMillis -import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.KmpUri -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.ByteArrayOutputStream - -class AndroidFileService( - private val context: KmpContext -) : FileService() { - override fun getFile(uri: KmpUri): PlatformFile? { - return AndroidFile(uri, context).takeIf { it.isExist() } - } - - override fun getCacheSizeInBytes(): Long { - return imageCacheDir.toFile().walkBottomUp().fold(0L) { acc, file -> acc + file.length() } - } - - override fun cleanCache() { - imageCacheDir.toFile().deleteRecursively() - } -} - -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/utils/KmpPlatform.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.android.kt index bece3050..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,7 +1,9 @@ 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.PlatformFile @@ -12,6 +14,19 @@ 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/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt index 54263a3f..1c6e72d5 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..7488f8bd 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.CompressFormat +import io.github.vinceglb.filekit.FileKit +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, + compressFormat = CompressFormat.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 b4f13603..f66152d1 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,27 +1,64 @@ package com.daniebeler.pfpixelix.domain.service.file +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.size +import me.tatarka.inject.annotations.Inject import okio.Path import okio.Path.Companion.toPath -abstract class FileService { - val dataStoreDir: Path = FileKit.filesDir.path.toPath().resolve("datastore") - val imageCacheDir: Path = FileKit.cacheDir.path.toPath().resolve("image_cache") +@Inject +class FileService( + private val context: KmpContext +) { + companion object { + val dataStoreDir = FileKit.filesDir.resolve("datastore") + val imageCacheDir = FileKit.cacheDir.resolve("image_cache") + } - abstract fun getFile(uri: KmpUri): PlatformFile? - abstract fun getCacheSizeInBytes(): Long - abstract fun cleanCache() + suspend fun getCacheSizeInBytes(): Long = imageCacheDir.sizeRecursively() + suspend fun cleanCache() { + imageCacheDir.deleteRecursively() + } + + 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/ui/composables/newpost/NewPostViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostViewModel.kt index e8679d61..768b4489 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) { 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/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 38bc08b2..59021680 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt @@ -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/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt index 62ff362a..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,11 +26,10 @@ fun AppViewController(urlCallback: IosUrlCallback): UIViewController { object : KmpContext() { override val viewController get() = viewController!! }, - IosFileService(), IosAppIconManager() ) - configureLogger(true) + configureLogger() SingletonImageLoader.setSafe { appComponent.provideImageLoader() 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 69eddab2..00000000 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/IosFileService.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.daniebeler.pfpixelix.domain.service.file - -import com.daniebeler.pfpixelix.utils.KmpUri -import io.github.vinceglb.filekit.FileKit -import io.github.vinceglb.filekit.cacheDir -import io.github.vinceglb.filekit.filesDir -import io.github.vinceglb.filekit.path -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() { - override fun getFile(uri: KmpUri): PlatformFile? { - return IosFile(uri).takeIf { it.isExist() } - } - - 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/utils/KmpPlatform.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.ios.kt index c15d6aa3..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 @@ -2,6 +2,16 @@ package com.daniebeler.pfpixelix.utils import coil3.PlatformContext 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/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt index 20a75c75..a647aa85 100644 --- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt @@ -9,11 +9,9 @@ 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 @@ -25,7 +23,6 @@ fun main() { val appComponent = AppComponent.Companion.create( object : KmpContext() {}, - DesktopFileService(), DesktopAppIconManager() ) 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 83236d75..00000000 --- a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/DesktopFileService.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.daniebeler.pfpixelix.domain.service.file - -import co.touchlab.kermit.Logger -import com.daniebeler.pfpixelix.utils.KmpUri -import io.github.vinceglb.filekit.FileKit -import io.github.vinceglb.filekit.cacheDir -import io.github.vinceglb.filekit.filesDir -import io.github.vinceglb.filekit.path -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() { - override fun getFile(uri: KmpUri): PlatformFile? { - return DesktopFile(uri).takeIf { it.isExist() } - } - - 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/utils/KmpPlatform.jvm.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.jvm.kt index 29f6984b..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 @@ -2,7 +2,10 @@ package com.daniebeler.pfpixelix.utils import coil3.PlatformContext 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4b019c0..c53a74f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,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" } From 49281b9da8d81007dd7ee5dde0ac28a29e52f7d2 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 18 Apr 2025 17:57:34 +0200 Subject: [PATCH 10/30] Save images in to the gallery and show snack message --- .../com/daniebeler/pfpixelix/AppActivity.kt | 3 + .../platform/PlatformFeatures.android.kt | 1 - .../kotlin/com/daniebeler/pfpixelix/App.kt | 108 ++++++++++-------- .../domain/service/file/FileDownloader.kt | 32 ------ .../domain/service/file/FileService.kt | 20 +++- .../service/platform/PlatformFeatures.kt | 1 - .../ui/composables/post/PostViewModel.kt | 11 +- .../ui/composables/post/ShareBottomSheet.kt | 18 +-- .../service/platform/PlatformFeatures.ios.kt | 1 - .../service/platform/PlatformFeatures.jvm.kt | 1 - 10 files changed, 96 insertions(+), 100 deletions(-) delete mode 100644 app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileDownloader.kt diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/AppActivity.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/AppActivity.kt index d3af2a4b..67214559 100644 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/AppActivity.kt +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/AppActivity.kt @@ -17,6 +17,8 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogWindowProvider +import io.github.vinceglb.filekit.FileKit +import io.github.vinceglb.filekit.dialogs.init import java.io.File import java.io.FileOutputStream import java.io.InputStream @@ -24,6 +26,7 @@ import java.lang.ref.WeakReference class AppActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + FileKit.init(this) MyApplication.currentActivity = WeakReference(this) super.onCreate(savedInstanceState) enableEdgeToEdge() 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 6a8cf22c..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 @@ -5,7 +5,6 @@ actual object PlatformFeatures { actual val inAppBrowser = true 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/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt index 525a28b8..99372e51 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,14 +28,13 @@ 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 @@ -55,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 @@ -90,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( @@ -125,51 +128,64 @@ fun App( var showAccountSwitchBottomSheet by remember { mutableStateOf(false) } val navController = rememberNavController() - ReverseModalNavigationDrawer( - gesturesEnabled = drawerState.isOpen, - drawerState = drawerState, - drawerContent = { - ModalDrawerSheet( - drawerState = drawerState, - drawerShape = shapes.extraLarge.end(0.dp), - ) { - PreferencesComposable(navController, drawerState, { - scope.launch { - drawerState.close() - } - }) - } + + val snackbarHostState = remember { SnackbarHostState() } + val snackBarPresenter: (String) -> Unit = { msg -> + scope.launch { + snackbarHostState.showSnackbar(msg) } + } + + CompositionLocalProvider( + LocalSnackbarPresenter provides snackBarPresenter ) { - Scaffold( - contentWindowInsets = WindowInsets(0), - 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 - ) - } - ) + 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 + ) + } + ) + } + ) + } } LaunchedEffect(Unit) { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileDownloader.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileDownloader.kt deleted file mode 100644 index 6fc7292c..00000000 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileDownloader.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.daniebeler.pfpixelix.domain.service.file - -import co.touchlab.kermit.Logger -import io.github.vinceglb.filekit.PlatformFile -import io.github.vinceglb.filekit.write -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.plugins.onDownload -import io.ktor.client.request.get -import io.ktor.client.statement.HttpResponse -import io.ktor.client.statement.bodyAsBytes -import io.ktor.client.statement.readBytes -import io.ktor.client.statement.readRawBytes -import io.ktor.utils.io.reader -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.IO -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.tatarka.inject.annotations.Inject - -@Inject -class FileDownloader(httpClient: HttpClient) { - private val client = httpClient.config { followRedirects = true } - fun download(file: PlatformFile, url: String) { - GlobalScope.launch(Dispatchers.IO) { - Logger.d { "Downloading: $url -> $file" } - val bytes = client.get(url).bodyAsBytes() - file.write(bytes) - } - } -} \ No newline at end of file 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 f66152d1..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,5 +1,6 @@ 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 @@ -15,25 +16,42 @@ 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 @Inject class FileService( - private val context: KmpContext + 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 } 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 { 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/post/PostViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/PostViewModel.kt index fcf3f7c8..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 @@ -12,7 +12,7 @@ import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.domain.model.ReportObjectType import com.daniebeler.pfpixelix.domain.service.account.AccountService import com.daniebeler.pfpixelix.domain.service.editor.PostEditorService -import com.daniebeler.pfpixelix.domain.service.file.FileDownloader +import com.daniebeler.pfpixelix.domain.service.file.FileService import com.daniebeler.pfpixelix.domain.service.platform.Platform import com.daniebeler.pfpixelix.domain.service.post.PostService import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences @@ -20,7 +20,6 @@ import com.daniebeler.pfpixelix.domain.service.session.AuthService import com.daniebeler.pfpixelix.domain.service.utils.Resource import com.daniebeler.pfpixelix.ui.composables.post.reply.OwnReplyState import com.daniebeler.pfpixelix.ui.composables.post.reply.RepliesState -import io.github.vinceglb.filekit.PlatformFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -36,7 +35,7 @@ class PostViewModel @Inject constructor( private val authService: AuthService, private val accountService: AccountService, private val platform: Platform, - private val fileDownloader: FileDownloader + private val fileService: FileService ) : ViewModel() { var post: Post? by mutableStateOf(null) @@ -434,8 +433,10 @@ class PostViewModel @Inject constructor( platform.openUrl(url) } - fun saveImage(file: PlatformFile, url: String) { - fileDownloader.download(file, 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 162839d6..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,13 +20,13 @@ 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 import com.daniebeler.pfpixelix.domain.service.platform.PlatformFeatures import com.daniebeler.pfpixelix.ui.composables.ButtonRowElement import com.daniebeler.pfpixelix.ui.navigation.Destination -import io.github.vinceglb.filekit.dialogs.compose.rememberFileSaverLauncher import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource @@ -43,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( @@ -134,20 +134,14 @@ fun ShareBottomSheet( PlatformFeatures.downloadToGallery && mediaAttachment?.url != null ) { - val fileSaverLauncher = rememberFileSaverLauncher { file -> - if (file != null) { - viewModel.saveImage(file, mediaAttachment.url) - } - closeBottomSheet() - } + val snackbarPresenter = LocalSnackbarPresenter.current ButtonRowElement( icon = Res.drawable.cloud_download_outline, text = stringResource(Res.string.download_image), onClick = { - fileSaverLauncher.launch( - suggestedName = post.account.username + "_" + mediaAttachment.id, - extension = mediaAttachment.url.substringAfterLast('.') - ) + viewModel.saveImage(mediaAttachment.url) + snackbarPresenter("Image saved to the gallery") + closeBottomSheet() } ) } 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 d19c2eed..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 @@ -5,7 +5,6 @@ actual object PlatformFeatures { actual val inAppBrowser = true 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/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 29bf7755..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 @@ -5,7 +5,6 @@ actual object PlatformFeatures { actual val inAppBrowser = 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 From 099a9bea9380a4ff096043c9644e6c0865e686a5 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 18 Apr 2025 18:02:24 +0200 Subject: [PATCH 11/30] Clear backstack states when switching users. --- .../kotlin/com/daniebeler/pfpixelix/App.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt index 99372e51..bb1612a5 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt @@ -128,7 +128,6 @@ fun App( var showAccountSwitchBottomSheet by remember { mutableStateOf(false) } val navController = rememberNavController() - val snackbarHostState = remember { SnackbarHostState() } val snackBarPresenter: (String) -> Unit = { msg -> scope.launch { @@ -136,6 +135,19 @@ fun App( } } + //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 ) { From 6da09f4d5dda7d9e7f10744069ce54ace84a549c Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 18 Apr 2025 19:10:58 +0200 Subject: [PATCH 12/30] Fix navigation after post creation. --- .../pfpixelix/ui/composables/newpost/NewPostViewModel.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 768b4489..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 @@ -287,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 -> { From e8bbf875f21cd94944a376c90f13a8258d48ecb5 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 25 Apr 2025 15:18:33 +0000 Subject: [PATCH 13/30] New Crowdin translations by GitHub Action --- app/src/commonMain/composeResources/values-pl-rPL/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7ef309b9fb446f365bd6cd2e509f3e2c65c59a53 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 25 Apr 2025 18:01:15 +0200 Subject: [PATCH 14/30] Update dependencies --- .../domain/service/editor/PostEditorService.kt | 4 ++-- gradle/libs.versions.toml | 18 +++++++++--------- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) 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 7488f8bd..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 @@ -7,8 +7,8 @@ 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.CompressFormat 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 @@ -38,7 +38,7 @@ class PostEditorService( quality = 85, maxWidth = 400, maxHeight = 400, - compressFormat = CompressFormat.PNG + imageFormat = ImageFormat.PNG ) } else null diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c53a74f1..38cb170b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,12 @@ [versions] kotlin = "2.1.20" ksp = "2.1.20-1.0.31" -agp = "8.9.1" +agp = "8.9.2" #https://github.com/JetBrains/compose-multiplatform/releases -composeMultiplatform = "1.8.10+dev2370" -lifecycleMultiplatform = "2.9.10+dev2370" -navigationMultiplatform = "2.9.10+dev2370" +composeMultiplatform = "1.8.0-rc01" +lifecycleMultiplatform = "2.9.0-alpha07" +navigationMultiplatform = "2.9.0-alpha17" #JetBrains kotlinx-coroutines = "1.10.2" @@ -18,15 +18,15 @@ kotlinx-datetime = "0.6.2" #multiplatform ksoup = "0.2.2" kermit = "2.0.5" -ktorfit = "2.5.0" +ktorfit = "2.5.1" kotlinInject = "0.7.2" androidx-annotation = "1.9.1" coil = "3.1.0" -datastorePreferences = "1.1.4" +datastorePreferences = "1.1.5" multiplatformSettings = "1.3.0" -filekitCompose = "0.10.0-beta01" +filekitCompose = "0.10.0-beta02" krop = "0.2.0-alpha02" -composemediaplayer = "0.6.4" +composemediaplayer = "0.7.1" #android accompanistSystemuicontroller = "0.36.0" @@ -36,7 +36,7 @@ coreKtx = "1.16.0" glance = "1.1.1" material = "1.12.0" okio = "3.11.0" -workRuntimeKtx = "2.10.0" +workRuntimeKtx = "2.10.1" #desktop slf4jSimple = "2.0.17" 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 From 6e0e9df6b30f28c12cb1a0b1d5032fdfcdd8c70e Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 25 Apr 2025 18:01:45 +0200 Subject: [PATCH 15/30] Hide volume toggle to videos with no audio tracks --- .../ui/composables/post/VideoAttachment.kt | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) 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 8160a4c4..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 @@ -66,27 +66,30 @@ fun VideoAttachment( } .isVisible(threshold = 50) { videoFrameIsVisible = it } ) - IconButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(8.dp), - onClick = { - viewModel.toggleVolume(!viewModel.volume) - }, - colors = IconButtonDefaults.filledTonalIconButtonColors() - ) { - if (viewModel.volume) { - Icon( - Icons.AutoMirrored.Outlined.VolumeUp, - contentDescription = "Volume on", - Modifier.size(18.dp) - ) - } else { - Icon( - Icons.AutoMirrored.Outlined.VolumeOff, - contentDescription = "Volume off", - Modifier.size(18.dp) - ) + val hasAudio = (player.metadata.audioChannels ?: 0) > 0 + if (hasAudio) { + IconButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(8.dp), + onClick = { + viewModel.toggleVolume(!viewModel.volume) + }, + colors = IconButtonDefaults.filledTonalIconButtonColors() + ) { + if (viewModel.volume) { + Icon( + Icons.AutoMirrored.Outlined.VolumeUp, + contentDescription = "Volume on", + Modifier.size(18.dp) + ) + } else { + Icon( + Icons.AutoMirrored.Outlined.VolumeOff, + contentDescription = "Volume off", + Modifier.size(18.dp) + ) + } } } } From 73970da3a233b28d3f922894eb818e3f26df1113 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 25 Apr 2025 18:08:28 +0200 Subject: [PATCH 16/30] Remove unused Compose Dev Maven repository --- settings.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 8f3d6c23..53ccade0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,7 +3,6 @@ pluginManagement { google() mavenCentral() gradlePluginPortal() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } dependencyResolutionManagement { @@ -11,7 +10,6 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } From ddf95df0c873a91f85a6abd21ff8a83e7d89a04b Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Tue, 6 May 2025 18:08:04 +0200 Subject: [PATCH 17/30] Update deps to the latest stable versions. --- app/build.gradle.kts | 6 ++++++ gradle.properties | 2 +- gradle/libs.versions.toml | 20 ++++++++++---------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a3fda0b..07d70b02 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 { @@ -238,3 +239,8 @@ compose.desktop { } } } + +tasks.configureEach { + if (this is KspAATask && name != "kspCommonMainKotlinMetadata") + dependsOn("kspCommonMainKotlinMetadata") +} \ No newline at end of file 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 38cb170b..25120919 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,32 +1,32 @@ [versions] kotlin = "2.1.20" -ksp = "2.1.20-1.0.31" +ksp = "2.1.20-2.0.1" agp = "8.9.2" #https://github.com/JetBrains/compose-multiplatform/releases -composeMultiplatform = "1.8.0-rc01" -lifecycleMultiplatform = "2.9.0-alpha07" -navigationMultiplatform = "2.9.0-alpha17" +composeMultiplatform = "1.8.0" +lifecycleMultiplatform = "2.9.0-beta01" +navigationMultiplatform = "2.9.0-beta01" #JetBrains kotlinx-coroutines = "1.10.2" kotlinxCollectionsImmutable = "0.3.8" kotlinxSerializationJson = "1.8.1" -ktor = "3.1.2" +ktor = "3.1.3" kotlinx-datetime = "0.6.2" #multiplatform -ksoup = "0.2.2" +ksoup = "0.2.3" kermit = "2.0.5" ktorfit = "2.5.1" -kotlinInject = "0.7.2" +kotlinInject = "0.8.0" androidx-annotation = "1.9.1" coil = "3.1.0" datastorePreferences = "1.1.5" multiplatformSettings = "1.3.0" -filekitCompose = "0.10.0-beta02" -krop = "0.2.0-alpha02" -composemediaplayer = "0.7.1" +filekitCompose = "0.10.0-beta03" +krop = "0.2.0-beta01" +composemediaplayer = "0.7.2" #android accompanistSystemuicontroller = "0.36.0" From 387aa7f9943a3b4d1dc2dcf1dc56892c8fa39daf Mon Sep 17 00:00:00 2001 From: cpbaumer Date: Tue, 6 May 2025 17:47:39 -0400 Subject: [PATCH 18/30] Fix typo in en-rUS strings.xml --- app/src/commonMain/composeResources/values-en-rUS/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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? From ebcf289d17ee30aaf10b74fd31bc9b644dff3810 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 16 May 2025 13:48:49 +0200 Subject: [PATCH 19/30] Treat all media types except video as image This commit updates the `PostComposable` to treat all media types other than "video" as "image". This ensures consistent handling of various media types and simplifies the logic for displaying and interacting with media attachments. --- .../pfpixelix/ui/composables/post/PostComposable.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f3baecac..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 @@ -731,7 +731,7 @@ fun PostImage( showMediaDialog = mediaAttachment }) }) { - if (mediaAttachment.type == "image") { + if (mediaAttachment.type != "video") { ImageWrapper( mediaAttachment, { zoomState.setContentSize(it.painter.intrinsicSize) }, @@ -848,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) }, From e084d3be97bcb725d44710652245a10f9bb53049 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 16 May 2025 14:14:21 +0200 Subject: [PATCH 20/30] Update various library versions. Key updates include: - Kotlin: 2.1.20 -> 2.1.21 - AGP: 8.9.2 -> 8.9.3 - kotlinxCollectionsImmutable: 0.3.8 -> 0.4.0 - ktorfit: 2.5.1 -> 2.5.2 - coil: 3.1.0 -> 3.2.0 - datastorePreferences: 1.1.5 -> 1.1.6 - filekitCompose: 0.10.0-beta03 -> 0.10.0-beta04 - krop: 0.2.0-beta01 -> 0.2.0 - composemediaplayer: 0.7.2 -> 0.7.4 --- gradle/libs.versions.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25120919..4829a1a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -kotlin = "2.1.20" +kotlin = "2.1.21" ksp = "2.1.20-2.0.1" -agp = "8.9.2" +agp = "8.9.3" #https://github.com/JetBrains/compose-multiplatform/releases composeMultiplatform = "1.8.0" @@ -10,7 +10,7 @@ navigationMultiplatform = "2.9.0-beta01" #JetBrains kotlinx-coroutines = "1.10.2" -kotlinxCollectionsImmutable = "0.3.8" +kotlinxCollectionsImmutable = "0.4.0" kotlinxSerializationJson = "1.8.1" ktor = "3.1.3" kotlinx-datetime = "0.6.2" @@ -18,15 +18,15 @@ kotlinx-datetime = "0.6.2" #multiplatform ksoup = "0.2.3" kermit = "2.0.5" -ktorfit = "2.5.1" +ktorfit = "2.5.2" kotlinInject = "0.8.0" androidx-annotation = "1.9.1" -coil = "3.1.0" -datastorePreferences = "1.1.5" +coil = "3.2.0" +datastorePreferences = "1.1.6" multiplatformSettings = "1.3.0" -filekitCompose = "0.10.0-beta03" -krop = "0.2.0-beta01" -composemediaplayer = "0.7.2" +filekitCompose = "0.10.0-beta04" +krop = "0.2.0" +composemediaplayer = "0.7.4" #android accompanistSystemuicontroller = "0.36.0" From 6806ad87cedb267a6231791f725316b41088d21a Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 16 May 2025 14:30:47 +0200 Subject: [PATCH 21/30] Update KSP and AGP versions, disable obfuscation in release builds - KSP updated to 2.1.21-2.0.1 - AGP updated to 8.10.0 - Added `getDefaultProguardFile("proguard-android-optimize.txt")` and `proguard-rules.pro` to release build type. - Added `-dontobfuscate` to `proguard-rules.pro` to prevent class name obfuscation, which was causing issues with some libraries. --- app/build.gradle.kts | 3 +++ app/proguard-rules.pro | 3 ++- gradle/libs.versions.toml | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 07d70b02..6dadd15c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -179,6 +179,9 @@ android { isDebuggable = false isProfileable = false isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + ) } } packaging.resources { 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4829a1a0..b8203b79 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.1.21" -ksp = "2.1.20-2.0.1" -agp = "8.9.3" +ksp = "2.1.21-2.0.1" +agp = "8.10.0" #https://github.com/JetBrains/compose-multiplatform/releases composeMultiplatform = "1.8.0" From 50a6783ddd3091b39fddae96074d4feb59a23f4d Mon Sep 17 00:00:00 2001 From: Hiebeler Date: Wed, 4 Jun 2025 20:54:53 +0200 Subject: [PATCH 22/30] #309 Bug fix: Fix opening compose window from sharing an image --- app/src/androidMain/AndroidManifest.xml | 15 +++++++++ .../com/daniebeler/pfpixelix/AppActivity.kt | 31 +++++++++---------- app/src/androidMain/res/xml/file_paths.xml | 6 ++++ 3 files changed, 35 insertions(+), 17 deletions(-) create mode 100644 app/src/androidMain/res/xml/file_paths.xml diff --git a/app/src/androidMain/AndroidManifest.xml b/app/src/androidMain/AndroidManifest.xml index e3330838..7250b5a3 100644 --- a/app/src/androidMain/AndroidManifest.xml +++ b/app/src/androidMain/AndroidManifest.xml @@ -41,6 +41,11 @@ + + + + + @@ -305,6 +310,16 @@ android:authorities="${applicationId}.androidx-startup" tools:node="remove"> + + + + { 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 { @@ -88,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 -> @@ -96,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() @@ -105,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 @@ -125,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 } @@ -144,7 +141,7 @@ private fun handleSharePhotoIntent( } imageUris = receivedUris?.mapNotNull { saveUriToCache( - it, contentResolver, cacheDir + it, contentResolver, cacheDir, appActivity ) } ?: emptyList() } 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 From 0cb22d1854c4770d74d225d5e1daa7021b68522e Mon Sep 17 00:00:00 2001 From: Hiebeler Date: Wed, 4 Jun 2025 21:23:45 +0200 Subject: [PATCH 23/30] #303 Fix bug when clicking on notification post --- .../ui/composables/notifications/CustomNotification.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ) ) }) From 7b0db3a68fb4f9f3402927df7eefe02ef5a1ca56 Mon Sep 17 00:00:00 2001 From: Hiebeler Date: Wed, 4 Jun 2025 21:49:59 +0200 Subject: [PATCH 24/30] Remove icon change two icons description --- .../icon_selection/IconSelectionComposable.kt | 11 ----------- 1 file changed, 11 deletions(-) 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), From 8dfe163b8abc048c1c7d67b96a799360c012eb1d Mon Sep 17 00:00:00 2001 From: Daniel Hiebeler Date: Sun, 8 Jun 2025 15:38:04 +0200 Subject: [PATCH 25/30] Increase version number --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6dadd15c..2b20fed0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -150,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 { From db447657e1abd8ec90bbd1e1ca9edac4bb930878 Mon Sep 17 00:00:00 2001 From: Daniel Hiebeler Date: Sun, 8 Jun 2025 16:01:01 +0200 Subject: [PATCH 26/30] Add app store badge for github readme --- appstorebadgewhite.svg | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 appstorebadgewhite.svg diff --git a/appstorebadgewhite.svg b/appstorebadgewhite.svg new file mode 100644 index 00000000..16c0496c --- /dev/null +++ b/appstorebadgewhite.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_wht_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a6c96c6dc26dda34787882364c5212d4c61989b9 Mon Sep 17 00:00:00 2001 From: Daniel Hiebeler Date: Sun, 8 Jun 2025 16:07:05 +0200 Subject: [PATCH 27/30] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 373abba9..2e78eda2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ 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. +Get it on Google Play Get it on F-Droid Get it on Google Play Get it from IzzyOnDroid From 520744bec70f4116347ca8eaac81118e12c06812 Mon Sep 17 00:00:00 2001 From: Daniel Hiebeler Date: Sun, 8 Jun 2025 16:07:36 +0200 Subject: [PATCH 28/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e78eda2..54cceef7 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Pixelix is an Android client for [Pixelfed](https://pixelfed.org/), the federate 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. Get it on Google Play -Get it on F-Droid Get it on Google Play +Get it on F-Droid Get it from IzzyOnDroid ## Please donate From a45ed8852ea9884ff33e3e616b0d8e89ae1cfabc Mon Sep 17 00:00:00 2001 From: Emanuel Hiebeler <78096107+Hiebeler@users.noreply.github.com> Date: Sun, 8 Jun 2025 16:16:06 +0200 Subject: [PATCH 29/30] Update crowdin.yml Update github token action permissions --- .github/workflows/crowdin.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From cc6777cc005b04c5e44de468fbdd62ef8b9c7716 Mon Sep 17 00:00:00 2001 From: Daniel Hiebeler Date: Sun, 8 Jun 2025 16:33:55 +0200 Subject: [PATCH 30/30] Adapt log level, Add patch notes --- .../kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt | 2 +- metadata/de/changelogs/32.txt | 4 ++++ metadata/en-US/changelogs/32.txt | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 metadata/de/changelogs/32.txt create mode 100644 metadata/en-US/changelogs/32.txt 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 1c6e72d5..12290f09 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt @@ -94,7 +94,7 @@ abstract class AppComponent( } } } - level = LogLevel.ALL + level = LogLevel.NONE } install(HttpTimeout) { requestTimeoutMillis = 60000 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