diff --git a/.gitignore b/.gitignore index 2da05054..af336a98 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ captures xcuserdata/ *.jks *.gpg +/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/iosApp 2025-03-27 12-51-49/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index fa487d5c..00000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Pixelix \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml deleted file mode 100644 index 09cfc854..00000000 --- a/.idea/appInsightsSettings.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 7643783a..00000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c..00000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56e..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 1ce1c320..00000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index ec5f8705..00000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 7b3006b6..00000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index e0da3ee5..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index d0667882..00000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml deleted file mode 100644 index f8051a6f..00000000 --- a/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 3b0be228..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 16660f1d..00000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ceca0bdb..b2f9b5d8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat.* + plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.kotlinMultiplatform) @@ -11,7 +13,7 @@ plugins { kotlin { jvmToolchain(17) androidTarget() - + jvm() listOf( iosX64(), iosArm64(), @@ -87,8 +89,6 @@ kotlin { //image loader implementation(libs.coil.compose) - implementation(libs.coil.video) - implementation(libs.coil.gif) implementation(libs.coil.network) //image crop @@ -104,7 +104,6 @@ kotlin { implementation(libs.androidx.core.ktx) implementation(libs.androidx.activity.compose) - implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.browser) implementation(libs.accompanist.systemuicontroller) @@ -116,6 +115,8 @@ kotlin { implementation(libs.androidx.media3.exoplayer.dash) implementation(libs.androidx.media3.ui) implementation(libs.android.image.cropper) + implementation(libs.coil.video) + implementation(libs.coil.gif) // widget implementation(libs.androidx.glance.appwidget) @@ -127,6 +128,21 @@ kotlin { iosMain.dependencies { implementation(libs.ktor.client.darwin) } + + jvmMain.dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutines.swing) + implementation(libs.ktor.client.okhttp) + implementation(libs.appdirs) + implementation(libs.slf4j.simple) + implementation(libs.vlcj) + implementation(libs.jna) + implementation(libs.jna.platform) + } + } + + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") } } @@ -138,8 +154,8 @@ android { applicationId = "com.daniebeler.pfpixelix" minSdk = 26 targetSdk = 35 - versionCode = 30 - versionName = "4.0.3" + versionCode = 31 + versionName = "4.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -177,6 +193,7 @@ android { dependencies { listOf( "kspAndroid", + "kspJvm", "kspIosX64", "kspIosArm64", "kspIosSimulatorArm64" @@ -184,3 +201,45 @@ dependencies { add(it, libs.kotlin.inject.compiler.ksp) } } + +compose.desktop { + application { + mainClass = "com.daniebeler.pfpixelix.MainKt" + + nativeDistributions { + targetFormats(Dmg, Msi, Deb) + packageName = "Pixelix" + packageVersion = "1.0.0" + + //data store https://issuetracker.google.com/280205600 + modules("jdk.unsupported") + modules("jdk.unsupported.desktop") + + linux { + iconFile.set(project.file("desktopAppIcons/LinuxIcon.png")) + } + windows { + iconFile.set(project.file("desktopAppIcons/WindowsIcon.ico")) + } + macOS { + iconFile.set(project.file("desktopAppIcons/MacosIcon.icns")) + bundleID = "com.daniebeler.pfpixelix" + infoPlist { + extraKeysRawXml = """ + CFBundleURLTypes + + + CFBundleURLName + Pixelix auth redirect + CFBundleURLSchemes + + pixelix-android-auth + + + + """.trimIndent() + } + } + } + } +} diff --git a/app/desktopAppIcons/LinuxIcon.png b/app/desktopAppIcons/LinuxIcon.png new file mode 100644 index 00000000..98f2c065 Binary files /dev/null and b/app/desktopAppIcons/LinuxIcon.png differ diff --git a/app/desktopAppIcons/MacosIcon.icns b/app/desktopAppIcons/MacosIcon.icns new file mode 100644 index 00000000..6f2c4c36 Binary files /dev/null and b/app/desktopAppIcons/MacosIcon.icns differ diff --git a/app/desktopAppIcons/WindowsIcon.ico b/app/desktopAppIcons/WindowsIcon.ico new file mode 100644 index 00000000..020bbf09 Binary files /dev/null and b/app/desktopAppIcons/WindowsIcon.ico differ diff --git a/app/src/androidMain/AndroidManifest.xml b/app/src/androidMain/AndroidManifest.xml index c21e23fc..6b3791a3 100644 --- a/app/src/androidMain/AndroidManifest.xml +++ b/app/src/androidMain/AndroidManifest.xml @@ -87,6 +87,16 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Unit, - properties: DialogProperties, - content: @Composable () -> Unit -) { - Dialog( - onDismissRequest = onDismissRequest, - properties = DialogProperties( - dismissOnBackPress = properties.dismissOnBackPress, - dismissOnClickOutside = properties.dismissOnClickOutside, - usePlatformDefaultWidth = true, - decorFitsSystemWindows = false - ), - content = { - SetUpEdgeToEdgeDialog() - content() - } - ) -} - -@Composable -private fun SetUpEdgeToEdgeDialog() { - val parentView = LocalView.current.parent as View - val window = (parentView as DialogWindowProvider).window - - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - - window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - window.attributes.fitInsetsTypes = 0 - window.attributes.fitInsetsSides = 0 - } -} +actual fun EdgeToEdgeDialogProperties( + dismissOnBackPress: Boolean, + dismissOnClickOutside: Boolean, + usePlatformDefaultWidth: Boolean +): DialogProperties = DialogProperties( + dismissOnBackPress = dismissOnBackPress, + dismissOnClickOutside = dismissOnClickOutside, + usePlatformDefaultWidth = usePlatformDefaultWidth, + decorFitsSystemWindows = false +) private fun saveUriToCache(uri: Uri, contentResolver: ContentResolver, cacheDir: File): Uri? { try { diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt index bd731dc6..f4d84c86 100644 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt @@ -3,6 +3,7 @@ package com.daniebeler.pfpixelix import android.app.Activity import android.app.Application import android.content.Context +import androidx.activity.ComponentActivity import androidx.work.Configuration import androidx.work.ListenableWorker import androidx.work.WorkerFactory @@ -10,6 +11,8 @@ 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 import com.daniebeler.pfpixelix.widget.notifications.work_manager.NotificationsTask @@ -23,7 +26,11 @@ class MyApplication : Application(), Configuration.Provider { get() = Configuration.Builder().setWorkerFactory(workerFactory).build() override fun onCreate() { - appComponent = AppComponent.create(this) + appComponent = AppComponent.create( + this, + AndroidFileService(this), + AndroidAppIconManager(this) + ) SingletonImageLoader.setSafe { appComponent.provideImageLoader() } @@ -34,7 +41,7 @@ class MyApplication : Application(), Configuration.Provider { companion object { lateinit var appComponent: AppComponent private set - var currentActivity: WeakReference? = null + var currentActivity: WeakReference? = null } } 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 new file mode 100644 index 00000000..1a31ef1d --- /dev/null +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/AndroidFileService.kt @@ -0,0 +1,226 @@ +package com.daniebeler.pfpixelix.domain.service.file + +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import android.widget.Toast +import co.touchlab.kermit.Logger +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.request.ImageRequest +import coil3.request.SuccessResult +import coil3.request.allowHardware +import coil3.toBitmap +import coil3.video.videoFrameMillis +import com.daniebeler.pfpixelix.utils.KmpContext +import com.daniebeler.pfpixelix.utils.KmpUri +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock +import okio.Path +import okio.Path.Companion.toPath +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileNotFoundException + +class AndroidFileService( + private val context: KmpContext +) : FileService { + override val dataStoreDir: Path = context.filesDir.path.toPath().resolve("datastore") + override val imageCacheDir: Path = context.cacheDir.path.toPath().resolve("image_cache") + + override fun getFile(uri: KmpUri): PlatformFile? { + return AndroidFile(uri, context).takeIf { it.isExist() } + } + + override fun downloadFile(name: String?, url: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + var uri: Uri? = null + val saveImageRoutine = CoroutineScope(Dispatchers.Default).launch { + + val bitmap: Bitmap? = urlToBitmap(url, context) + if (bitmap == null) { + cancel("an error occured when downloading the image") + return@launch + } + + uri = saveImageToMediaStore( + context, + generateUniqueName(name, false, context), + bitmap + ) + if (uri == null) { + cancel("an error occured when saving the image") + return@launch + } + } + + saveImageRoutine.invokeOnCompletion { throwable -> + CoroutineScope(Dispatchers.Main).launch { + uri?.let { + Toast.makeText(context, "Stored at: " + uri.toString(), Toast.LENGTH_LONG) + .show() + } ?: throwable?.let { + Toast.makeText( + context, "an error occurred downloading the image", Toast.LENGTH_LONG + ).show() + } + } + } + } + } + + override fun getCacheSizeInBytes(): Long { + return imageCacheDir.toFile().walkBottomUp().fold(0L) { acc, file -> acc + file.length() } + } + + override fun cleanCache() { + imageCacheDir.toFile().deleteRecursively() + } + + private fun generateUniqueName( + imageName: String?, returnFullPath: Boolean, context: KmpContext + ): String { + + val filename = "${imageName}_${Clock.System.now().epochSeconds}" + + if (returnFullPath) { + val directory: File = context.getDir("zest", Context.MODE_PRIVATE) + return "$directory/$filename" + } else { + return filename + } + } + + private suspend fun urlToBitmap( + imageURL: String, + context: KmpContext, + ): Bitmap? { + val loader = ImageLoader(context) + val request = ImageRequest.Builder(context).data(imageURL).allowHardware(false).build() + val result = loader.execute(request) + if (result is SuccessResult) { + return result.image.toBitmap() + } + return null + } + + private fun saveImageToMediaStore( + context: KmpContext, + displayName: String, + bitmap: Bitmap + ): Uri? { + val imageCollections = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + } else { + MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + + val imageDetails = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, displayName) + put(MediaStore.Images.Media.MIME_TYPE, "image/png") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaStore.Images.Media.IS_PENDING, 1) + } + } + + val resolver = context.applicationContext.contentResolver + val imageContentUri = resolver.insert(imageCollections, imageDetails) ?: return null + + return try { + resolver.openOutputStream(imageContentUri, "w").use { os -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, os!!) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + imageDetails.clear() + imageDetails.put(MediaStore.Images.Media.IS_PENDING, 0) + resolver.update(imageContentUri, imageDetails, null, null) + } + + imageContentUri + } catch (e: FileNotFoundException) { + // Some legacy devices won't create directory for the Uri if dir not exist, resulting in + // a FileNotFoundException. To resolve this issue, we should use the File API to save the + // image, which allows us to create the directory ourselves. + null + } + } +} + +private class AndroidFile( + private val uri: Uri, + private val context: Context +) : PlatformFile { + override fun isExist(): Boolean = + getName() != "AndroidFile:unknown" + + override fun getName(): String = when (uri.scheme) { + ContentResolver.SCHEME_FILE -> uri.pathSegments.last().substringBeforeLast('.') + ContentResolver.SCHEME_CONTENT -> context.contentResolver.query( + uri, null, null, null, null + )?.use { + val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + it.moveToFirst() + it.getString(nameIndex) + } + + else -> null + } ?: "AndroidFile:unknown" + + override fun getSize(): Long = when (uri.scheme) { + ContentResolver.SCHEME_FILE -> context.contentResolver.openFileDescriptor(uri, "r") + ?.use { it.statSize } + + ContentResolver.SCHEME_CONTENT -> context.contentResolver.query( + uri, null, null, null, null + )?.use { + val nameIndex = it.getColumnIndex(OpenableColumns.SIZE) + it.moveToFirst() + it.getLong(nameIndex) + } + + else -> null + } ?: 0L + + override fun getMimeType(): String = when (uri.scheme) { + ContentResolver.SCHEME_FILE -> { + val fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()) + MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase()) + } + + ContentResolver.SCHEME_CONTENT -> { + context.contentResolver.getType(uri) + } + + else -> null + } ?: "image/*" + + override suspend fun readBytes(): ByteArray = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri)!!.readBytes() + } + + override suspend fun getThumbnail(): ByteArray? = withContext(Dispatchers.IO) { + val bm = try { + val req = ImageRequest.Builder(context).data(uri).videoFrameMillis(0).build() + val img = SingletonImageLoader.get(context).execute(req) + img.image?.toBitmap() + } catch (e: Exception) { + Logger.e("AndroidFile.getThumbnail error", e) + null + } ?: return@withContext null + + val stream = ByteArrayOutputStream() + bm.compress(Bitmap.CompressFormat.PNG, 100, stream) + stream.toByteArray() + } +} \ No newline at end of file diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AndroidAppIconManager.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AndroidAppIconManager.kt new file mode 100644 index 00000000..9b8c4c07 --- /dev/null +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AndroidAppIconManager.kt @@ -0,0 +1,69 @@ +package com.daniebeler.pfpixelix.domain.service.icon + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import co.touchlab.kermit.Logger +import org.jetbrains.compose.resources.DrawableResource +import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.app_icon_00 +import pixelix.app.generated.resources.app_icon_01 +import pixelix.app.generated.resources.app_icon_02 +import pixelix.app.generated.resources.app_icon_03 +import pixelix.app.generated.resources.app_icon_05 +import pixelix.app.generated.resources.app_icon_06 +import pixelix.app.generated.resources.app_icon_07 +import pixelix.app.generated.resources.app_icon_08 +import pixelix.app.generated.resources.app_icon_09 + +class AndroidAppIconManager( + private val context: Context +) : AppIconManager { + private val iconIds = mapOf( + Res.drawable.app_icon_00 to "com.daniebeler.pfpixelix.Icon04", + Res.drawable.app_icon_01 to "com.daniebeler.pfpixelix.Icon01", + Res.drawable.app_icon_02 to "com.daniebeler.pfpixelix.AppActivity", + Res.drawable.app_icon_03 to "com.daniebeler.pfpixelix.Icon03", + Res.drawable.app_icon_05 to "com.daniebeler.pfpixelix.Icon05", + Res.drawable.app_icon_06 to "com.daniebeler.pfpixelix.Icon06", + Res.drawable.app_icon_07 to "com.daniebeler.pfpixelix.Icon07", + Res.drawable.app_icon_08 to "com.daniebeler.pfpixelix.Icon08", + Res.drawable.app_icon_09 to "com.daniebeler.pfpixelix.Icon09", + ) + + override fun getCurrentIcon(): DrawableResource { + for ((res, id) in iconIds.entries) { + val i = context.packageManager.getComponentEnabledSetting(ComponentName(context, id)) + if (i == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + return res + } + } + return Res.drawable.app_icon_02 + } + + override fun setCustomIcon(icon: DrawableResource) { + try { + val pm = context.packageManager + for ((res, id) in iconIds.entries) { + if (res != icon) { + val i = pm.getComponentEnabledSetting(ComponentName(context, id)) + if (i == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + pm.setComponentEnabledSetting( + ComponentName(context, id), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } + } else { + pm.setComponentEnabledSetting( + ComponentName(context, iconIds[icon]!!), + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) + } + } + } catch (e: Error) { + Logger.e("enableCustomIcon", e) + } + } +} \ No newline at end of file diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.android.kt index 9a7173e1..f1c9b754 100644 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.android.kt +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.android.kt @@ -2,68 +2,21 @@ package com.daniebeler.pfpixelix.domain.service.platform import android.appwidget.AppWidgetManager import android.content.ComponentName -import android.content.ContentResolver -import android.content.ContentValues -import android.content.Context import android.content.Intent -import android.content.pm.PackageManager -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 androidx.browser.customtabs.CustomTabsIntent 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.MyApplication import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.KmpUri import com.daniebeler.pfpixelix.widget.notifications.NotificationWidgetReceiver -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 me.tatarka.inject.annotations.Inject -import org.jetbrains.compose.resources.DrawableResource -import pixelix.app.generated.resources.Res -import pixelix.app.generated.resources.app_icon_00 -import pixelix.app.generated.resources.app_icon_01 -import pixelix.app.generated.resources.app_icon_02 -import pixelix.app.generated.resources.app_icon_03 -import pixelix.app.generated.resources.app_icon_05 -import pixelix.app.generated.resources.app_icon_06 -import pixelix.app.generated.resources.app_icon_07 -import pixelix.app.generated.resources.app_icon_08 -import pixelix.app.generated.resources.app_icon_09 -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.FileNotFoundException @Inject actual class Platform actual constructor( private val context: KmpContext, private val prefs: UserPreferences ) { - actual fun getPlatformFile(uri: KmpUri): PlatformFile? { - val f = AndroidFile(uri, context) - return if (f.getName() != "AndroidFile:unknown") f else null - } - - actual fun getAppIconManager(): AppIconManager { - return AndroidAppIconManager(context) - } - actual fun openUrl(url: String) { val activity = MyApplication.currentActivity?.get() if (activity != null) { @@ -94,7 +47,9 @@ actual class Platform actual constructor( } } - actual fun getAppVersion(): String { + actual fun dismissBrowser() {} + + actual fun getAppVersion(): String { return try { context.packageManager.getPackageInfo(context.packageName, 0).versionName } catch (e: Exception) { @@ -111,237 +66,4 @@ actual class Platform actual constructor( appWidgetManager.requestPinAppWidget(myProvider, null, null) } } - - actual fun downloadImageToGallery(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 - } - - println(bitmap.toString()) - - 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() - } - } - } - } - } - - 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 - } - } - - actual fun getCacheSizeInBytes(): Long { - return context.cacheDir.walkBottomUp().fold(0L) { acc, file -> acc + file.length() } - } - - actual fun cleanCache() { - context.cacheDir.deleteRecursively() - } -} - -private class AndroidFile( - private val uri: Uri, - private val context: Context -) : PlatformFile { - 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() - } } - -private class AndroidAppIconManager( - private val context: Context -) : AppIconManager { - private val iconIds = mapOf( - Res.drawable.app_icon_00 to "com.daniebeler.pfpixelix.Icon04", - Res.drawable.app_icon_01 to "com.daniebeler.pfpixelix.Icon01", - Res.drawable.app_icon_02 to "com.daniebeler.pfpixelix.AppActivity", - Res.drawable.app_icon_03 to "com.daniebeler.pfpixelix.Icon03", - Res.drawable.app_icon_05 to "com.daniebeler.pfpixelix.Icon05", - Res.drawable.app_icon_06 to "com.daniebeler.pfpixelix.Icon06", - Res.drawable.app_icon_07 to "com.daniebeler.pfpixelix.Icon07", - Res.drawable.app_icon_08 to "com.daniebeler.pfpixelix.Icon08", - Res.drawable.app_icon_09 to "com.daniebeler.pfpixelix.Icon09", - ) - - override fun getCurrentIcon(): DrawableResource { - for ((res, id) in iconIds.entries) { - val i = context.packageManager.getComponentEnabledSetting(ComponentName(context, id)) - if (i == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { - return res - } - } - return Res.drawable.app_icon_02 - } - - override fun setCustomIcon(icon: DrawableResource) { - try { - val pm = context.packageManager - for ((res, id) in iconIds.entries) { - if (res != icon) { - val i = pm.getComponentEnabledSetting(ComponentName(context, id)) - if (i == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { - pm.setComponentEnabledSetting( - ComponentName(context, id), - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP - ) - } - } else { - pm.setComponentEnabledSetting( - ComponentName(context, iconIds[icon]!!), - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP - ) - } - } - } catch (e: Error) { - Logger.e("enableCustomIcon", e) - } - } -} \ No newline at end of file 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 94ccf91b..f6ee25f4 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 @@ -6,4 +6,8 @@ actual object PlatformFeatures { actual val notificationWidgets = true actual val inAppBrowser = true actual val downloadToGallery = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + actual val customAppIcon = true + actual val autoplayVideosPref = true + actual val addCollection = true + actual val customAccentColors = false } \ No newline at end of file diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.android.kt index 4de94b36..a3e45a53 100644 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.android.kt +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.android.kt @@ -5,67 +5,56 @@ import android.os.Build import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge -import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatDelegate import androidx.compose.material3.ColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.core.view.WindowInsetsControllerCompat +import co.touchlab.kermit.Logger +import com.daniebeler.pfpixelix.MyApplication +import com.daniebeler.pfpixelix.di.LocalAppComponent import com.daniebeler.pfpixelix.domain.model.AppThemeMode.AMOLED import com.daniebeler.pfpixelix.domain.model.AppThemeMode.DARK import com.daniebeler.pfpixelix.domain.model.AppThemeMode.LIGHT -import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.LocalKmpContext -actual fun KmpContext.generateColorScheme( +@Composable +actual fun generateColorScheme( nightModeValue: Int, dynamicColor: Boolean, lightScheme: ColorScheme, darkScheme: ColorScheme -): ColorScheme = if (dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - when (nightModeValue) { - AMOLED -> dynamicDarkColorScheme(this).toAmoled() - DARK -> dynamicDarkColorScheme(this) - else -> dynamicLightColorScheme(this) - } -} else { - when (nightModeValue) { - AMOLED -> darkScheme.toAmoled() - DARK -> darkScheme - else -> lightScheme - } -} - -actual fun applySystemNightMode(mode: Int) { - AppCompatDelegate.setDefaultNightMode( - when (mode) { - LIGHT -> AppCompatDelegate.MODE_NIGHT_NO - AMOLED, DARK -> AppCompatDelegate.MODE_NIGHT_YES - else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM +): ColorScheme { + val context = LocalAppComponent.current.context + return remember( + nightModeValue, dynamicColor, lightScheme, darkScheme + ) { + if (dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + when (nightModeValue) { + AMOLED -> dynamicDarkColorScheme(context).toAmoled() + DARK -> dynamicDarkColorScheme(context) + else -> dynamicLightColorScheme(context) + } + } else { + when (nightModeValue) { + AMOLED -> darkScheme.toAmoled() + DARK -> darkScheme + else -> lightScheme + } } - ) + } } -@Composable -actual fun ChangeSystemBarColors(mode: Int) { - (LocalKmpContext.current as ComponentActivity).enableEdgeToEdge( - when (mode) { - LIGHT -> SystemBarStyle.light( - Color.Transparent.toArgb(), Color.Transparent.toArgb() - ) - - AMOLED, DARK -> SystemBarStyle.dark( - Color.Transparent.toArgb() - ) - - else -> SystemBarStyle.dark( - Color.Transparent.toArgb() - ) - } - - ) - +actual fun applySystemNightMode(isDark: Boolean) { + val activity = MyApplication.currentActivity?.get() ?: return + val window = activity.window + Logger.d { "applySystemNightMode isDark=$isDark" } + WindowInsetsControllerCompat(window, window.decorView).apply { + isAppearanceLightStatusBars = !isDark + isAppearanceLightNavigationBars = !isDark + } } \ 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 3c209fe8..3ce9c589 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 @@ -5,9 +5,6 @@ import android.net.Uri import androidx.core.net.toUri import coil3.PlatformContext import io.github.vinceglb.filekit.core.PlatformFile -import okio.Path -import okio.Path.Companion.toPath -import java.io.File actual typealias KmpUri = Uri actual val EmptyKmpUri: KmpUri = Uri.EMPTY @@ -17,7 +14,3 @@ actual fun PlatformFile.toKmpUri(): KmpUri = this.uri actual typealias KmpContext = Context actual val KmpContext.coilContext: PlatformContext get() = this -actual val KmpContext.dataStoreDir: Path - get() = File(applicationContext.filesDir, "datastore").path.toPath() -actual val KmpContext.imageCacheDir: Path - get() = cacheDir.path.toPath().resolve("image_cache") 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 index 26539f14..86860231 100644 --- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.android.kt +++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.android.kt @@ -2,6 +2,7 @@ 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 @@ -13,6 +14,7 @@ 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 @@ -24,6 +26,7 @@ actual class VideoPlayer actual constructor( ) { 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() @@ -48,6 +51,10 @@ actual class VideoPlayer actual constructor( } } } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + isVideoPlaying?.invoke(isPlaying) + } }) } @@ -67,6 +74,9 @@ actual class VideoPlayer actual constructor( @OptIn(UnstableApi::class) @Composable actual fun view(modifier: Modifier) { + LaunchedEffect(player) { + player.isPlaying + } AndroidView( modifier = modifier, factory = { ctx -> diff --git a/app/src/commonMain/composeResources/drawable/blur.xml b/app/src/commonMain/composeResources/drawable/blur.xml new file mode 100644 index 00000000..939aff7b --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/blur.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/commonMain/composeResources/drawable/sync_outline_bold.xml b/app/src/commonMain/composeResources/drawable/sync_outline_bold.xml new file mode 100644 index 00000000..cf7a297e --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/sync_outline_bold.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/app/src/commonMain/composeResources/drawable/trash.xml b/app/src/commonMain/composeResources/drawable/trash.xml new file mode 100644 index 00000000..76205fce --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/trash.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/drawable/warning.xml b/app/src/commonMain/composeResources/drawable/warning.xml new file mode 100644 index 00000000..531b7831 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/warning.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/app/src/commonMain/composeResources/values-af-rZA/strings.xml b/app/src/commonMain/composeResources/values-af-rZA/strings.xml index 6cc98a41..260b83ab 100644 --- a/app/src/commonMain/composeResources/values-af-rZA/strings.xml +++ b/app/src/commonMain/composeResources/values-af-rZA/strings.xml @@ -18,7 +18,19 @@ Follow Unfollow Followers + + Follower + Followers + Following + + Following + Following + + + Post + Posts + Posts Hashtags reblogged your post @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-ar-rSA/strings.xml b/app/src/commonMain/composeResources/values-ar-rSA/strings.xml index 143125e8..a8c6658a 100644 --- a/app/src/commonMain/composeResources/values-ar-rSA/strings.xml +++ b/app/src/commonMain/composeResources/values-ar-rSA/strings.xml @@ -18,7 +18,31 @@ متابعة إلغاء المُتابعة متابِعون + + Followers + Follower + Followers + Followers + Followers + Followers + يتابع + + Following + Following + Following + Following + Following + Following + + + Posts + Post + Posts + Posts + Posts + Posts + المنشورات الوسوم reblogged your post @@ -216,4 +240,83 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + seconds + second + seconds + seconds + seconds + seconds + + + minutes + minute + minutes + minutes + minutes + minutes + + + hours + hour + hours + hours + hours + hours + + + days + day + days + days + days + days + + + weeks + week + weeks + weeks + weeks + weeks + + + months + month + months + months + months + months + + + years + year + years + years + years + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-ca-rES/strings.xml b/app/src/commonMain/composeResources/values-ca-rES/strings.xml index efd8ab74..c13171f3 100644 --- a/app/src/commonMain/composeResources/values-ca-rES/strings.xml +++ b/app/src/commonMain/composeResources/values-ca-rES/strings.xml @@ -18,7 +18,19 @@ Segueix Deixa de seguir Seguidors + + Follower + Followers + Següent + + Following + Following + + + Post + Posts + Entrades Etiquetes ha reblogjat la teva publicació @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-cs-rCZ/strings.xml b/app/src/commonMain/composeResources/values-cs-rCZ/strings.xml index 9b998259..0eec7024 100644 --- a/app/src/commonMain/composeResources/values-cs-rCZ/strings.xml +++ b/app/src/commonMain/composeResources/values-cs-rCZ/strings.xml @@ -18,7 +18,25 @@ Sledovat Zrušit sledování Sledující + + Follower + Followers + Followers + Followers + Sleduje + + Following + Following + Following + Following + + + Post + Posts + Posts + Posts + Příspěvky Hashtagy reblogoval tvůj příspěvek @@ -216,4 +234,69 @@ Schová popisky, liky a tlačítka u příspěvků. Nastavení repostu AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + seconds + seconds + + + minute + minutes + minutes + minutes + + + hour + hours + hours + hours + + + day + days + days + days + + + week + weeks + weeks + weeks + + + month + months + months + months + + + year + years + years + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-da-rDK/strings.xml b/app/src/commonMain/composeResources/values-da-rDK/strings.xml index f7ab73d9..995ae8dd 100644 --- a/app/src/commonMain/composeResources/values-da-rDK/strings.xml +++ b/app/src/commonMain/composeResources/values-da-rDK/strings.xml @@ -18,7 +18,19 @@ Følg Følg ikke Followers + + Follower + Followers + Following + + Following + Following + + + Post + Posts + Posts Hashtags reblogged your post @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-de-rDE/strings.xml b/app/src/commonMain/composeResources/values-de-rDE/strings.xml index 645c1607..3a22ca8a 100644 --- a/app/src/commonMain/composeResources/values-de-rDE/strings.xml +++ b/app/src/commonMain/composeResources/values-de-rDE/strings.xml @@ -18,7 +18,19 @@ Folgen Entfolgen Follower + + Follower + Followers + Gefolgt + + Following + Following + + + Beitrag + Beiträge + Beiträge Hashtags hat deinen Beitrag geteilt @@ -206,14 +218,65 @@ %1$s Version %2$s Besuche %1$s Anzeigename - Bio + Über mich Website Standort Erwähnungen Entdecken geteilt von %1$s - Focus Mode - Hides description, likes and buttons from posts. + Fokusmodus + Versteckt Beschreibung, Likes und Buttons von Beiträgen. Boost-Einstellungen AMOLED + Noch keine Follower + Sie folgen niemanden + Mehr lesen + Weniger anzeigen + Beigetreten %1$s + her + Nachricht löschen + Video-Player wird nicht unterstützt + + Sekunde + Sekunden + + + Minute + Minuten + + + Stunde + Stunden + + + Tag + Tage + + + Woche + Wochen + + + Monat + Monate + + + Jahr + Jahre + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-el-rGR/strings.xml b/app/src/commonMain/composeResources/values-el-rGR/strings.xml index e043bc94..6e5c925e 100644 --- a/app/src/commonMain/composeResources/values-el-rGR/strings.xml +++ b/app/src/commonMain/composeResources/values-el-rGR/strings.xml @@ -18,7 +18,19 @@ Ακολούθησε Άρση ακολούθησης Ακόλουθοι + + Ακόλουθος + Ακόλουθοι + Ακολουθείς + + Ακολουθεί + Ακολουθούν + + + Ανάρτηση + Αναρτήσεις + Αναρτήσεις Ετικέτες αναδημοσίευσε την ανάρτησή σου @@ -216,4 +228,55 @@ Κρύβει την περιγραφή, μου αρέσει και κουμπιά από αναρτήσεις. Ρυθμίσεις αναδημοσίευσης AMOLED + Κανένας ακόλουθος ακόμα + Δεν ακολουθείται κανένας + Διάβασε περισσότερα + Διάβασε Λιγότερα + Έγινε Μέλος %1$s + πριν + Διαγραφή μηνύματος + Ο αναπαραγωγέας βίντεο δεν υποστηρίζεται + + δευτερόλεπτο + δευτερόλεπτα + + + λεπτό + λεπτά + + + ώρα + ώρες + + + ημέρα + ημέρες + + + εβδομάδα + εβδομάδες + + + μήνας + μήνες + + + έτος + έτη + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-en-rUS/strings.xml b/app/src/commonMain/composeResources/values-en-rUS/strings.xml index 6cc98a41..260b83ab 100644 --- a/app/src/commonMain/composeResources/values-en-rUS/strings.xml +++ b/app/src/commonMain/composeResources/values-en-rUS/strings.xml @@ -18,7 +18,19 @@ Follow Unfollow Followers + + Follower + Followers + Following + + Following + Following + + + Post + Posts + Posts Hashtags reblogged your post @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-eo-rUY/strings.xml b/app/src/commonMain/composeResources/values-eo-rUY/strings.xml index 6cc98a41..260b83ab 100644 --- a/app/src/commonMain/composeResources/values-eo-rUY/strings.xml +++ b/app/src/commonMain/composeResources/values-eo-rUY/strings.xml @@ -18,7 +18,19 @@ Follow Unfollow Followers + + Follower + Followers + Following + + Following + Following + + + Post + Posts + Posts Hashtags reblogged your post @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-es-rES/strings.xml b/app/src/commonMain/composeResources/values-es-rES/strings.xml index 4d3e71d0..467ab369 100644 --- a/app/src/commonMain/composeResources/values-es-rES/strings.xml +++ b/app/src/commonMain/composeResources/values-es-rES/strings.xml @@ -18,7 +18,19 @@ Seguir No seguir Seguidores + + Seguidor + Seguidores + Siguiendo + + Siguiendo + Siguiendo + + + Publicación + Publicaciones + Publicaciones Hashtags reblogueó tu publicación @@ -216,4 +228,55 @@ Oculta la descripción, me gusta y los botones de los mensajes. Configuración de volver a publicar AMOLED + Todavía no hay seguidores + No sigues a nadie + Leer más + Leer menos + Se unió el %1$s + hace + Eliminar mensaje + El reproductor de vídeo no es compatible + + segundo + segundos + + + minuto + minutos + + + hora + horas + + + día + días + + + semana + semanas + + + mes + meses + + + año + años + + Difuminar contenido sensible + Reproducir automáticamente los videos + Reportar esta publicación + Reportar + Spam + Contenido para adultos o sensible + Abusivo o dañino + Cuenta para menores + Violencia + Violación de derechos de autor + Suplantación de la identidad + Estafa + Terrorismo + Reporte enviado + Eliminar la cuenta diff --git a/app/src/commonMain/composeResources/values-fi-rFI/strings.xml b/app/src/commonMain/composeResources/values-fi-rFI/strings.xml index f2c95dc3..09830f8a 100644 --- a/app/src/commonMain/composeResources/values-fi-rFI/strings.xml +++ b/app/src/commonMain/composeResources/values-fi-rFI/strings.xml @@ -18,7 +18,19 @@ Seuraa Lopeta seuraaminen Seuraajat + + Follower + Followers + Seurataan + + Following + Following + + + Post + Posts + Julkaisut Aihetunnisteet jakoi julkaisusi @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-fr-rFR/strings.xml b/app/src/commonMain/composeResources/values-fr-rFR/strings.xml index 403dd5e1..02926154 100644 --- a/app/src/commonMain/composeResources/values-fr-rFR/strings.xml +++ b/app/src/commonMain/composeResources/values-fr-rFR/strings.xml @@ -18,7 +18,19 @@ Suivre Ne plus suivre Abonné·e·s + + Abonné + Abonné·e·s + Abonnements + + Abonnements + Abonnements + + + Publication + Publications + Publications Hashtags a republié votre publication @@ -28,8 +40,8 @@ Tendances Comptes Règles - Version de l\'instance - Conditions générales d\'utilisation + Version de l’instance + Conditions générales d’utilisation Politique de Confidentialité Administrateur·rice Utilisateur·rice·s @@ -54,24 +66,24 @@ tous les ans tous les mois tous les jours - Je n\'ai pas de compte + Je n’ai pas de compte URL du serveur Voulez-vous vraiment vous déconnecter ? Se déconnecter ? Afficher %1$s commentaires Laisser un commentaire Aucun commentaire pour l\'instant - Personne n\'a aimé pour l\'instant + Personne n’a aimé pour l’instant aucune publication aimée Local Fédéré Accueil - %1$s J\'aime + %1$s J’aime Rechercher Ne plus masquer ne plus masquer Description du média - Aucune publication pour l\'instant + Aucune publication pour l’instant vous a mentionné·e Aucune publication favorite Rien à afficher @@ -81,7 +93,7 @@ légende Média suggestif/sensible avertissement de contenu ou texte caché - Audience + Visibilité non répertorié abonné·e·s seulement public @@ -102,12 +114,12 @@ Explorer les profils tendances Personne ne vous suit actuellement Vide - Suivez des comptes ou hashtags pour remplir votre fil d\'actualité + Suivez des comptes ou hashtags pour remplir votre fil d’actualité Aucune publication - Le fil d\'accueil vous montre les publications des personnes et hashtags que vous suivez + Le fil d’accueil vous montre les publications des personnes et hashtags que vous suivez Le fil local affiche les messages de tous les utilisateur·rices de votre serveur Le fil fédéré affiche les publications des utilisateur·rices de tous les serveurs fédérés au vôtre - Cet utilisateur·rice n\'a pas posté pour l\'instant + Cet utilisateur·rice n’a pas posté pour l\'instant Aucune nouvelle notification voir plus Modifier le profil @@ -118,31 +130,31 @@ Pixelfed est un réseau de partage de photos et d\'images sur le Fediverse avec une interface orientée photo, qui comprend des albums, des filtres, des moments etc. Mastodon est une plateforme de microblogging libre et open source PeerTube est un outil de partage de vidéos en ligne développé par Framasoft, un organisme à but non lucratif français. - Lemmy est une plate-forme auto-hébergée de discussion et d\'agrégation de liens. Elle est entièrement libre et ouverte, et non contrôlée par aucune entreprise. Cela signifie qu\'il n\'y a pas d\'algorithmes secret, de publicité ou de tracking. Le contenu est organisé en communautés, il est donc facile de s\'abonner à des sujets qui vous intéressent, et d\'ignorer les autres. Le vote est utilisé pour faire remonter les sujets les plus intéressants. - Threads est une application de Meta avec laquelle vous pouvez voir et partager publiquement des conversations. Vous pouvez également poster des fils, répondre à d\'autres personnes et suivre des profils qui vous intéressent. + Lemmy est une plate-forme auto-hébergée de discussion et d\'agrégation de liens. Elle est entièrement libre et ouverte, et non contrôlée par aucune entreprise. Cela signifie qu’il n’y a pas d\'algorithmes secret, de publicité ou de tracking. Le contenu est organisé en communautés, il est donc facile de s’abonner à des sujets qui vous intéressent, et d’ignorer les autres. Le vote est utilisé pour faire remonter les sujets les plus intéressants. + Threads est une application de Meta avec laquelle vous pouvez voir et partager publiquement des conversations. Vous pouvez également poster des fils, répondre à d’autres personnes et suivre des profils qui vous intéressent. Plus d\'informations - Qu\'est-ce qui rend une publication tendance ? + Qu’est-ce qui rend une publication tendance ? Les publications sont tendances si elles ont été postées pendant une période sélectionnée et ont beaucoup de J\'aime. - La section \"comptes tendances\" liste les comptes sur votre instance avec le plus d\'abonné·e·s. Les comptes que vous suivez déjà sont cachés. - Qu\'est-ce qui rend un compte tendance ? - Qu\'est-ce qui rend un hashtag tendance ? + La section “comptes tendances”liste les comptes sur votre instance avec le plus d’abonné·e·s. Les comptes que vous suivez déjà sont cachés. + Qu’est-ce qui rend un compte tendance ? + Qu’est-ce qui rend un hashtag tendance ? Tendance des hashtags si beaucoup d\'utilisateur·rice·s les ont utilisés récemment. Profil privé - • Vous ne verrez pas l\'utilisateur·rice dans votre fil d\'actualité - • Vous ne verrez pas d\'autres personnes republier l\'utilisateur - • Vous ne verrez pas d\'autres personnes mentionner l\'utilisateur·rice - • Vous ne verrez pas l\'utilisateur·rice dans les fils publics - L\'utilisateur n\'a aucun moyen de savoir qu\'il a été masqué. - • Vous ne verrez pas l\'utilisateur·rice dans votre fil d\'actualité - • Vous ne verrez pas d\'autres personnes republier l\'utilisateur - • Vous ne verrez pas d\'autres personnes mentionner l\'utilisateur·rice - • Vous ne verrez pas l\'utilisateur·rice dans les fils publics + • Vous ne verrez pas l\'’utilisateur·rice dans votre fil d\'actualité + • Vous ne verrez pas d’autres personnes republier l’utilisateur + • Vous ne verrez pas d’autres personnes mentionner l’utilisateur·rice + • Vous ne verrez pas l’utilisateur·rice dans les fils publics + L\'utilisateur n’a aucun moyen de savoir qu\'il a été masqué. + • Vous ne verrez pas l’utilisateur·rice dans votre fil d’actualité + • Vous ne verrez pas d’autres personnes republier l’utilisateur + • Vous ne verrez pas d’autres personnes mentionner l’utilisateur·rice + • Vous ne verrez pas l’utilisateur·rice dans les fils publics • Vous ne verrez pas de notifications provenant de cet utilisateur·rice - • L\'utilisateur·rice est automatiquement désabonné - • L\'utilisateur·rice ne peut plus vous suivre - • L\'utilisateur·rice ne verra pas les republications d\'autres personnes de vous - • L\'utilisateur·rice ne vous verra pas dans les fils publics - Si vous et l\'utilisateur·rice bloqué·e sont sur le même serveur, l\'utilisateur·rice bloqué·e ne pourra pas voir vos messages sur votre profil en étant connecté. + • L’utilisateur·rice est automatiquement désabonné + • L’utilisateur·rice ne peut plus vous suivre + • L’utilisateur·rice ne verra pas les republications d’autres personnes de vous + • L’utilisateur·rice ne vous verra pas dans les fils publics + Si vous et l’utilisateur·rice bloqué·e sont sur le même serveur, l’utilisateur·rice bloqué·e ne pourra pas voir vos messages sur votre profil en étant connecté. Bloquer Bloquer le compte ? Ne plus masquer le compte ? @@ -157,9 +169,9 @@ Sélectionner un destinataire Nouveau Message Privé Avertissement - "Les messages privés sur Pixelfed ne sont pas chiffrés de bout en bout. Soyez prudent·e·s lorsque vous partagez des données sensibles. " + "Les messages privés sur Pixelfed ne sont pas chiffrés de bout en bout. Soyez prudent·e·s lorsque vous partagez des données sensibles." Confirmer - Ceci est le début de votre conversation avec cet·te utilisateur·rice. N\'oubliez pas d\'être respectueux. + Ceci est le début de votre conversation avec cet·te utilisateur·rice. N’oubliez pas d’être respectueux. Évaluez-nous À propos de Pixelix Développé par @@ -167,7 +179,7 @@ Partager cette collection Par %1$s Nouvelle Collection - La création de collections n\'est pas encore disponible dans l\'api de Pixelfed, mais une nouvelle collection peut être créée sur l\'application web de Pixelfed : + La création de collections n\'est pas encore disponible dans l’API de Pixelfed, mais une nouvelle collection peut être créée sur l’application web de Pixelfed : Nouveau Collections Vous suit @@ -175,17 +187,17 @@ bloqué·e Message Tout - J\'aime + J’aime Contenu partagé Modifier cette publication - Restez à jour avec vos dernières notifications de Pixelfed directement sur votre écran d\'accueil - Personnaliser l\'icône de l\'application - Changer l\'icône de l\'application ? - L\'icône peut mettre un certain temps à s\'afficher. + Restez à jour avec vos dernières notifications de Pixelfed directement sur votre écran d’accueil + Personnaliser l’icône de l’application + Changer l’icône de l’application ? + L’icône peut mettre un certain temps à s’afficher. Modifier vous a mentionné·e - jetez un coup d\'œil à la dernière publication de votre fil d\'actualité, directement de votre écran d\'accueil - Si une icône autre que celle par défaut est sélectionnée, deux icônes s\'afficheront dans votre tiroir d\'applications. + jetez un coup d’œil à la dernière publication de votre fil d’actualité, directement de votre écran d\'accueil + Si une icône autre que celle par défaut est sélectionnée, deux icônes s’afficheront dans votre tiroir d’applications. Compte Actif : Autres comptes : Ajouter un compte Pixeled @@ -201,19 +213,70 @@ Cacher le bouton texte alternatif Instances : Nombre total de publications : - Nombre total d\'utilisateur·rices : + Nombre total d’utilisateur·rices : utilisateur·rices actif·ves : %1$s version %2$s Se rendre sur %1$s Pseudonyme Description - Site Internet + Site web Localisation Mentions Explorer reblogged by %1$s Mode sans distraction - Masque la description, les J\'aime et les boutons des messages. - Repost settings + Masque la description, les J’aime et les boutons des messages. + Paramètres de republication AMOLED + Aucun abonné pour le moment + Vous ne suivez personne + Lire la suite + En voir moins + Inscrit·e le %1$s + ago + Supprimer le message + Lecteur vidéo non pris en charge + + seconde + secondes + + + minute + minutes + + + heure + heures + + + jour + jours + + + semaine + semaines + + + mois + mois + + + année + années + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-gl-rES/strings.xml b/app/src/commonMain/composeResources/values-gl-rES/strings.xml index 8b7a3470..4d4bac14 100644 --- a/app/src/commonMain/composeResources/values-gl-rES/strings.xml +++ b/app/src/commonMain/composeResources/values-gl-rES/strings.xml @@ -18,7 +18,19 @@ Seguir Deixar de seguir Seguidoras + + Seguidora + Seguidoras + Seguindo + + Seguimento + Seguimentos + + + Publicación + Publicacións + Publicacións Cancelos compartiu a túa publicación @@ -211,9 +223,60 @@ Localización Mencións Descubrir - reblogged by %1$s - Focus Mode - Hides description, likes and buttons from posts. - Repost settings + compartido por %1$s + Modo de Concentración + Oculta a descrición, os favorecementos e os botóns nas publicacións. + Axustes das promocións AMOLED + Sen seguidoras + Sen seguimentos + Ler máis + Ler menos + Creada hai %1$s + ago + Eliminar mensaxe + Video player is not supported + + segundo + segundos + + + minuto + minutos + + + hora + horas + + + día + días + + + semana + semanas + + + mes + meses + + + ano + anos + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-hu-rHU/strings.xml b/app/src/commonMain/composeResources/values-hu-rHU/strings.xml index 4bfa394c..bbb93f51 100644 --- a/app/src/commonMain/composeResources/values-hu-rHU/strings.xml +++ b/app/src/commonMain/composeResources/values-hu-rHU/strings.xml @@ -18,7 +18,19 @@ Követés Követés vége Követő + + Követő + Követő + Követett + + Követett + Követett + + + Bejegyzés + Bejegyzés + Bejegyzés Hashtagek megosztotta a bejegyzésed @@ -216,4 +228,55 @@ Elrejti a bejegyzések alól a leírást, a kedveléseket és a gombokat. Megosztások beállítása AMOLED + Még nincsenek követői + Nem követ senkit + Bővebben + Rövidebben + Csatlakozás ideje %1$s + ezelőtt + Üzenet törlése + Nem támogatott videólejátszó + + másodperccel + másodperccel + + + perccel + perccel + + + órával + órával + + + nappal + nappal + + + héttel + héttel + + + hónappal + hónappal + + + évvel + évvel + + Érzékeny tartalom elhomályosítása + Videók automatikus lejátszása + Bejegyzés jelentése + Jelentés + Spam + Felnőtt vagy érzékeny tartalom + Bántalmazó vagy káros + Kiskorú fiók + Erőszak + Szerzői jog megsértése + Hamis személyazonosság + Csalás + Terrorizmus + Jelentve + Fiók törlése diff --git a/app/src/commonMain/composeResources/values-io-rEN/strings.xml b/app/src/commonMain/composeResources/values-io-rEN/strings.xml index 6cc98a41..260b83ab 100644 --- a/app/src/commonMain/composeResources/values-io-rEN/strings.xml +++ b/app/src/commonMain/composeResources/values-io-rEN/strings.xml @@ -18,7 +18,19 @@ Follow Unfollow Followers + + Follower + Followers + Following + + Following + Following + + + Post + Posts + Posts Hashtags reblogged your post @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-it-rIT/strings.xml b/app/src/commonMain/composeResources/values-it-rIT/strings.xml index 06662c93..015bb83c 100644 --- a/app/src/commonMain/composeResources/values-it-rIT/strings.xml +++ b/app/src/commonMain/composeResources/values-it-rIT/strings.xml @@ -18,7 +18,19 @@ Segui Smetti di seguire Seguaci + + Follower + Followers + Seguiti + + Following + Following + + + Post + Posts + Post Hashtag ha ricondiviso il tuo post @@ -216,4 +228,55 @@ Nasconde le descrizioni, i \"mi piace\" e i pulsanti dai post. Impostazioni di ripubblicazione AMOLED + Ancora nessun seguace + Not following anyone + Read More + Read Less + Joined %1$s + fa + Elimina messaggio + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-iw-rIL/strings.xml b/app/src/commonMain/composeResources/values-iw-rIL/strings.xml index 6cc98a41..a5c0bc55 100644 --- a/app/src/commonMain/composeResources/values-iw-rIL/strings.xml +++ b/app/src/commonMain/composeResources/values-iw-rIL/strings.xml @@ -18,7 +18,25 @@ Follow Unfollow Followers + + Follower + Followers + Followers + Followers + Following + + Following + Following + Following + Following + + + Post + Posts + Posts + Posts + Posts Hashtags reblogged your post @@ -216,4 +234,69 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + seconds + seconds + + + minute + minutes + minutes + minutes + + + hour + hours + hours + hours + + + day + days + days + days + + + week + weeks + weeks + weeks + + + month + months + months + months + + + year + years + years + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-ja-rJP/strings.xml b/app/src/commonMain/composeResources/values-ja-rJP/strings.xml index 6cc98a41..8ab1324a 100644 --- a/app/src/commonMain/composeResources/values-ja-rJP/strings.xml +++ b/app/src/commonMain/composeResources/values-ja-rJP/strings.xml @@ -18,7 +18,16 @@ Follow Unfollow Followers + + Followers + Following + + Following + + + Posts + Posts Hashtags reblogged your post @@ -216,4 +225,48 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + seconds + + + minutes + + + hours + + + days + + + weeks + + + months + + + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-ko-rKR/strings.xml b/app/src/commonMain/composeResources/values-ko-rKR/strings.xml index 6cc98a41..8ab1324a 100644 --- a/app/src/commonMain/composeResources/values-ko-rKR/strings.xml +++ b/app/src/commonMain/composeResources/values-ko-rKR/strings.xml @@ -18,7 +18,16 @@ Follow Unfollow Followers + + Followers + Following + + Following + + + Posts + Posts Hashtags reblogged your post @@ -216,4 +225,48 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + seconds + + + minutes + + + hours + + + days + + + weeks + + + months + + + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-nl-rNL/strings.xml b/app/src/commonMain/composeResources/values-nl-rNL/strings.xml index cc036b6b..804d2c8f 100644 --- a/app/src/commonMain/composeResources/values-nl-rNL/strings.xml +++ b/app/src/commonMain/composeResources/values-nl-rNL/strings.xml @@ -18,7 +18,19 @@ Volgen Stop met volgen Volgers + + Follower + Followers + Gevolgd + + Following + Following + + + Post + Posts + Berichten Hashtags deelde je bericht @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-no-rNO/strings.xml b/app/src/commonMain/composeResources/values-no-rNO/strings.xml index c4839d4e..c1bc613c 100644 --- a/app/src/commonMain/composeResources/values-no-rNO/strings.xml +++ b/app/src/commonMain/composeResources/values-no-rNO/strings.xml @@ -18,7 +18,19 @@ Følg Avfølg Følgere + + Follower + Followers + Følger + + Following + Following + + + Post + Posts + Innlegg Emneknagger reblogged your post @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-pl-rPL/strings.xml b/app/src/commonMain/composeResources/values-pl-rPL/strings.xml index a62144d2..7345ef66 100644 --- a/app/src/commonMain/composeResources/values-pl-rPL/strings.xml +++ b/app/src/commonMain/composeResources/values-pl-rPL/strings.xml @@ -18,7 +18,25 @@ Obserwuj Przestań obserwować Obserwujące + + Obserwujący + Obserwujące + Obserwujących + Obserwujących + Obserwowane + + Obserwowany + Obserwowane + Obserwowanych + Obserwowanych + + + Wpis + Wpisy + Wpisów + Wpisów + Wpisy Hasztagi podbił(a) twój wpis @@ -48,7 +66,7 @@ Usunąć To już koniec! oraz - Następnie + Obserwują: inne inne rocznie @@ -216,4 +234,69 @@ Ukrywa opisy, polubienia i przyciski z wpisów. Ustawienia podbić AMOLED + Na razie brak obserwujących kont + Brak obserwowanych kont + Więcej + Mniej + Data dołączenia: %1$s + temu + Usuń wiadomość + Odtwarzacz wideo nie jest obsługiwany + + sekundę + sekundy + sekund + sekund + + + minutę + minuty + minut + minut + + + godzinę + godziny + godzin + godzin + + + dzień + dni + dni + dni + + + tydzień + tygodnie + tygodni + tygodni + + + miesiąc + miesiące + miesięcy + miesięcy + + + rok + lata + lat + lat + + Rozmyj drażliwe treści + Automatycznie odtwarzaj wideo + Zgłoś ten wpis + Zgłoś + Spam + Zawartość drażliwa lub dla dorosłych + Obraźliwe lub krzywdzące + Konto osoby niepełnoletniej + Przemoc + Naruszenie praw autorskich + Podszywanie się + Scam + Terroryzm + Zgłoszono + Delete Account diff --git a/app/src/commonMain/composeResources/values-pt-rBR/strings.xml b/app/src/commonMain/composeResources/values-pt-rBR/strings.xml index 56b99a60..9e9decc7 100644 --- a/app/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/app/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -18,7 +18,19 @@ Seguir Deixar de seguir Seguidores + + Follower + Followers + Seguindo + + Following + Following + + + Post + Posts + Postagens Hashtags compartilhou sua publicação @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-pt-rPT/strings.xml b/app/src/commonMain/composeResources/values-pt-rPT/strings.xml index 9bb14948..39d4e6db 100644 --- a/app/src/commonMain/composeResources/values-pt-rPT/strings.xml +++ b/app/src/commonMain/composeResources/values-pt-rPT/strings.xml @@ -18,7 +18,19 @@ Seguir Deixar de seguir Seguidores + + Follower + Followers + Seguindo + + Following + Following + + + Post + Posts + Publicações Etiquetas impulsionou a tua publicação @@ -216,4 +228,55 @@ Oculta a descrição, gostos e botões das publicações. Configurações de impulsos AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-ro-rRO/strings.xml b/app/src/commonMain/composeResources/values-ro-rRO/strings.xml index bfab0c7f..6c01831a 100644 --- a/app/src/commonMain/composeResources/values-ro-rRO/strings.xml +++ b/app/src/commonMain/composeResources/values-ro-rRO/strings.xml @@ -18,7 +18,22 @@ Follow Unfollow Followers + + Follower + Followers + Followers + Following + + Following + Following + Following + + + Post + Posts + Posts + Posts Hashtags reblogged your post @@ -216,4 +231,62 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + seconds + + + minute + minutes + minutes + + + hour + hours + hours + + + day + days + days + + + week + weeks + weeks + + + month + months + months + + + year + years + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-ru-rRU/strings.xml b/app/src/commonMain/composeResources/values-ru-rRU/strings.xml index fddfc0a2..799814b4 100644 --- a/app/src/commonMain/composeResources/values-ru-rRU/strings.xml +++ b/app/src/commonMain/composeResources/values-ru-rRU/strings.xml @@ -18,7 +18,25 @@ Подписаться Отписаться Подписчиков + + Follower + Followers + Followers + Followers + В читаемых + + Following + Following + Following + Following + + + Post + Posts + Posts + Posts + Записей Хештеги переопубликовал вашу запись @@ -216,4 +234,69 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + seconds + seconds + + + minute + minutes + minutes + minutes + + + hour + hours + hours + hours + + + day + days + days + days + + + week + weeks + weeks + weeks + + + month + months + months + months + + + year + years + years + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-sr-rSP/strings.xml b/app/src/commonMain/composeResources/values-sr-rSP/strings.xml index 6cc98a41..a9d18d17 100644 --- a/app/src/commonMain/composeResources/values-sr-rSP/strings.xml +++ b/app/src/commonMain/composeResources/values-sr-rSP/strings.xml @@ -18,7 +18,22 @@ Follow Unfollow Followers + + Follower + Followers + Followers + Following + + Following + Following + Following + + + Post + Posts + Posts + Posts Hashtags reblogged your post @@ -216,4 +231,62 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + seconds + + + minute + minutes + minutes + + + hour + hours + hours + + + day + days + days + + + week + weeks + weeks + + + month + months + months + + + year + years + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/src/commonMain/composeResources/values-sv-rSE/strings.xml index 95264320..7590d33c 100644 --- a/app/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -18,7 +18,19 @@ Följa Sluta följa Anhängare + + Follower + Followers + Följande + + Following + Following + + + Post + Posts + Inlägg Hashtags rebloggade ditt inlägg @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-tlh-rAA/strings.xml b/app/src/commonMain/composeResources/values-tlh-rAA/strings.xml index 6cc98a41..260b83ab 100644 --- a/app/src/commonMain/composeResources/values-tlh-rAA/strings.xml +++ b/app/src/commonMain/composeResources/values-tlh-rAA/strings.xml @@ -18,7 +18,19 @@ Follow Unfollow Followers + + Follower + Followers + Following + + Following + Following + + + Post + Posts + Posts Hashtags reblogged your post @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-tr-rTR/strings.xml b/app/src/commonMain/composeResources/values-tr-rTR/strings.xml index 6cc98a41..260b83ab 100644 --- a/app/src/commonMain/composeResources/values-tr-rTR/strings.xml +++ b/app/src/commonMain/composeResources/values-tr-rTR/strings.xml @@ -18,7 +18,19 @@ Follow Unfollow Followers + + Follower + Followers + Following + + Following + Following + + + Post + Posts + Posts Hashtags reblogged your post @@ -216,4 +228,55 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-uk-rUA/strings.xml b/app/src/commonMain/composeResources/values-uk-rUA/strings.xml index 41e00c90..8dac5d54 100644 --- a/app/src/commonMain/composeResources/values-uk-rUA/strings.xml +++ b/app/src/commonMain/composeResources/values-uk-rUA/strings.xml @@ -18,7 +18,25 @@ Підписатися Відписатися Підписники + + Follower + Followers + Followers + Followers + Слідкую + + Following + Following + Following + Following + + + Post + Posts + Posts + Posts + Публікації Хештеґи репост вашого допису @@ -212,8 +230,73 @@ Згадування Досліджуйте репост від %1$s - Focus Mode - Hides description, likes and buttons from posts. - Repost settings + Режим фокусу + Приховує опис, кнопки \"подобається\" і \". + Налаштування репостів AMOLED + Поки немає підписників! + Нікого не відстежую + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + seconds + seconds + + + minute + minutes + minutes + minutes + + + hour + hours + hours + hours + + + day + days + days + days + + + week + weeks + weeks + weeks + + + month + months + months + months + + + year + years + years + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-vi-rVN/strings.xml b/app/src/commonMain/composeResources/values-vi-rVN/strings.xml index 6cc98a41..8ab1324a 100644 --- a/app/src/commonMain/composeResources/values-vi-rVN/strings.xml +++ b/app/src/commonMain/composeResources/values-vi-rVN/strings.xml @@ -18,7 +18,16 @@ Follow Unfollow Followers + + Followers + Following + + Following + + + Posts + Posts Hashtags reblogged your post @@ -216,4 +225,48 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + seconds + + + minutes + + + hours + + + days + + + weeks + + + months + + + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-zh-rCN/strings.xml b/app/src/commonMain/composeResources/values-zh-rCN/strings.xml index 6cc98a41..8ab1324a 100644 --- a/app/src/commonMain/composeResources/values-zh-rCN/strings.xml +++ b/app/src/commonMain/composeResources/values-zh-rCN/strings.xml @@ -18,7 +18,16 @@ Follow Unfollow Followers + + Followers + Following + + Following + + + Posts + Posts Hashtags reblogged your post @@ -216,4 +225,48 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + seconds + + + minutes + + + hours + + + days + + + weeks + + + months + + + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values-zh-rTW/strings.xml b/app/src/commonMain/composeResources/values-zh-rTW/strings.xml index 6cc98a41..8ab1324a 100644 --- a/app/src/commonMain/composeResources/values-zh-rTW/strings.xml +++ b/app/src/commonMain/composeResources/values-zh-rTW/strings.xml @@ -18,7 +18,16 @@ Follow Unfollow Followers + + Followers + Following + + Following + + + Posts + Posts Hashtags reblogged your post @@ -216,4 +225,48 @@ Hides description, likes and buttons from posts. Repost settings AMOLED + No followers yet + Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + seconds + + + minutes + + + hours + + + days + + + weeks + + + months + + + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/composeResources/values/strings.xml b/app/src/commonMain/composeResources/values/strings.xml index 73b2f698..7006afe9 100644 --- a/app/src/commonMain/composeResources/values/strings.xml +++ b/app/src/commonMain/composeResources/values/strings.xml @@ -19,7 +19,19 @@ Follow Unfollow Followers + + Follower + Followers + Following + + Following + Following + + + Post + Posts + Posts Hashtags reblogged your post @@ -219,4 +231,53 @@ AMOLED No followers yet Not following anyone + Read More + Read Less + Joined %1$s + ago + Delete message + Video player is not supported + + second + seconds + + + minute + minutes + + + hour + hours + + + day + days + + + week + weeks + + + month + months + + + year + years + + Blur sensitive content + Autoplay Videos + Report this post + Report + Spam + Adult or Sensitive Content + Abusive or Harmful + Underage Account + Violence + Copyright infringement + Impersonation + Scam + Terrorism + Reported + Delete Account diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt index bfda2d85..edf6c469 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt @@ -1,11 +1,8 @@ package com.daniebeler.pfpixelix -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn 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 @@ -30,12 +27,14 @@ 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.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -48,61 +47,47 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.dialog import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import co.touchlab.kermit.Logger import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.di.AppComponent import com.daniebeler.pfpixelix.di.LocalAppComponent -import com.daniebeler.pfpixelix.ui.composables.HomeComposable import com.daniebeler.pfpixelix.ui.composables.ReverseModalNavigationDrawer -import com.daniebeler.pfpixelix.ui.composables.collection.CollectionComposable -import com.daniebeler.pfpixelix.ui.composables.direct_messages.chat.ChatComposable -import com.daniebeler.pfpixelix.ui.composables.direct_messages.conversations.ConversationsComposable -import com.daniebeler.pfpixelix.ui.composables.edit_post.EditPostComposable -import com.daniebeler.pfpixelix.ui.composables.edit_profile.EditProfileComposable -import com.daniebeler.pfpixelix.ui.composables.explore.ExploreComposable -import com.daniebeler.pfpixelix.ui.composables.followers.FollowersMainComposable -import com.daniebeler.pfpixelix.ui.composables.mention.MentionComposable -import com.daniebeler.pfpixelix.ui.composables.newpost.NewPostComposable -import com.daniebeler.pfpixelix.ui.composables.notifications.NotificationsComposable -import com.daniebeler.pfpixelix.ui.composables.profile.other_profile.OtherProfileComposable import com.daniebeler.pfpixelix.ui.composables.profile.own_profile.AccountSwitchBottomSheet -import com.daniebeler.pfpixelix.ui.composables.profile.own_profile.OwnProfileComposable -import com.daniebeler.pfpixelix.ui.composables.session.LoginComposable -import com.daniebeler.pfpixelix.ui.composables.settings.about_instance.AboutInstanceComposable -import com.daniebeler.pfpixelix.ui.composables.settings.about_pixelix.AboutPixelixComposable -import com.daniebeler.pfpixelix.ui.composables.settings.blocked_accounts.BlockedAccountsComposable -import com.daniebeler.pfpixelix.ui.composables.settings.bookmarked_posts.BookmarkedPostsComposable -import com.daniebeler.pfpixelix.ui.composables.settings.followed_hashtags.FollowedHashtagsComposable -import com.daniebeler.pfpixelix.ui.composables.settings.icon_selection.IconSelectionComposable -import com.daniebeler.pfpixelix.ui.composables.settings.liked_posts.LikedPostsComposable -import com.daniebeler.pfpixelix.ui.composables.settings.muted_accounts.MutedAccountsComposable import com.daniebeler.pfpixelix.ui.composables.settings.preferences.PreferencesComposable -import com.daniebeler.pfpixelix.ui.composables.single_post.SinglePostComposable -import com.daniebeler.pfpixelix.ui.composables.timelines.hashtag_timeline.HashtagTimelineComposable +import com.daniebeler.pfpixelix.ui.navigation.Destination +import com.daniebeler.pfpixelix.ui.navigation.appGraph import com.daniebeler.pfpixelix.ui.theme.PixelixTheme -import com.daniebeler.pfpixelix.utils.Destinations -import com.daniebeler.pfpixelix.utils.KmpUri -import com.daniebeler.pfpixelix.utils.Navigate import com.daniebeler.pfpixelix.utils.end -import com.daniebeler.pfpixelix.utils.toKmpUri import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.add_circle +import pixelix.app.generated.resources.add_circle_outline +import pixelix.app.generated.resources.bookmark_outline import pixelix.app.generated.resources.default_avatar +import pixelix.app.generated.resources.home +import pixelix.app.generated.resources.house +import pixelix.app.generated.resources.house_fill +import pixelix.app.generated.resources.new_post +import pixelix.app.generated.resources.notifications +import pixelix.app.generated.resources.notifications_outline +import pixelix.app.generated.resources.profile +import pixelix.app.generated.resources.search +import pixelix.app.generated.resources.search_outline @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -166,25 +151,27 @@ fun App( ) }, content = { paddingValues -> + val startDestination = + if (activeUser == null) Destination.FirstLogin + else Destination.HomeTabFeeds NavHost( modifier = Modifier.fillMaxSize().padding(paddingValues) .consumeWindowInsets(WindowInsets.navigationBars), navController = navController, - startDestination = Destinations.FirstLogin.route, + startDestination = startDestination, builder = { - navigationGraph( + appGraph( navController, { scope.launch { drawerState.open() } }, exitApp ) } ) + val launchUser = remember { activeUser } LaunchedEffect(activeUser) { - val rootScreen = if (activeUser == null) { - Destinations.FirstLogin.route - } else { - Destinations.HomeScreen.route - } + if (launchUser == activeUser) return@LaunchedEffect + val rootScreen = + if (activeUser == null) Destination.FirstLogin else Destination.HomeTabFeeds navController.navigate(rootScreen) { val root = navController.currentBackStack.value .firstOrNull { it.destination.route != null } @@ -196,11 +183,8 @@ fun App( if (activeUser != null) { appComponent.systemFileShare.shareFilesRequests.collect { uris -> - val urisJson = Json.encodeToString( - uris.map { uri -> uri.toString() } - ) - Navigate.navigate( - "new_post_screen?uris=$urisJson", navController + navController.navigate( + Destination.NewPost(uris.map { it.toString() }) ) } } @@ -225,242 +209,49 @@ fun App( } } -private fun NavGraphBuilder.navigationGraph( - navController: NavHostController, - openPreferencesDrawer: () -> Unit, - exitApp: () -> Unit +private enum class HomeTab( + val destination: Destination, + val icon: DrawableResource, + val activeIcon: DrawableResource, + val label: StringResource ) { - dialog( - route = Destinations.FirstLogin.route, - ) { - EdgeToEdgeDialog( - onDismissRequest = exitApp, - properties = DialogProperties( - dismissOnClickOutside = false, - usePlatformDefaultWidth = false, - ) - ) { - LoginComposable(navController = navController) - } - } - dialog(route = Destinations.NewLogin.route) { - EdgeToEdgeDialog( - onDismissRequest = { navController.popBackStack() }, - properties = DialogProperties( - dismissOnClickOutside = false, - usePlatformDefaultWidth = false, - ) - ) { - LoginComposable(true, navController) - } - } - - composable(Destinations.HomeScreen.route) { - HomeComposable(navController, openPreferencesDrawer) - } - - composable(Destinations.NotificationsScreen.route) { - NotificationsComposable(navController) - } - - composable(Destinations.Profile.route) { navBackStackEntry -> - /* val uId = navBackStackEntry.arguments?.read { - if (hasValue("userid")) getString("userid") else null - }*/ - val uId = navBackStackEntry.arguments?.getString("userid") - - uId?.let { id -> - OtherProfileComposable(navController, userId = id, byUsername = null) - - } - } - - composable(Destinations.ProfileByUsername.route) { navBackStackEntry -> - /*val username = navBackStackEntry.arguments?.read { - if (hasValue("username")) getString("username") else null - } -*/ - val username = navBackStackEntry.arguments?.getString("username") - - username?.let { - OtherProfileComposable(navController, userId = "", byUsername = it) - } - } - - composable(Destinations.Hashtag.route) { navBackStackEntry -> - /*val uId = navBackStackEntry.arguments?.read { - if (hasValue("hashtag")) getString("hashtag") else null - }*/ - val uId = navBackStackEntry.arguments?.getString("hashtag") - - uId?.let { id -> - HashtagTimelineComposable(navController, id) - } - } - - composable(Destinations.EditProfile.route) { - EditProfileComposable(navController) - } - - composable(Destinations.IconSelection.route) { - IconSelectionComposable(navController) - } - - composable("${Destinations.NewPost.route}?uris={uris}") { navBackStackEntry -> - /*val urisJson = navBackStackEntry.arguments?.read { - if (hasValue("uris")) getString("uris") else null - } -*/ - val urisJson = navBackStackEntry.arguments?.getString("uris") - - val imageUris: List? = urisJson?.let { json -> - Json.decodeFromString>(json).map { it.toKmpUri() } - } - NewPostComposable(navController, imageUris) - } - - composable(Destinations.EditPost.route) { navBackStackEntry -> - /*val postId = navBackStackEntry.arguments?.read { - if (hasValue("postId")) getString("postId") else null - } -*/ - val postId = navBackStackEntry.arguments?.getString("postId") - postId?.let { id -> - EditPostComposable(postId, navController) - } - } - - composable(Destinations.MutedAccounts.route) { - MutedAccountsComposable(navController) - } - - composable(Destinations.BlockedAccounts.route) { - BlockedAccountsComposable(navController) - } - - composable(Destinations.LikedPosts.route) { - LikedPostsComposable(navController) - } - - composable(Destinations.BookmarkedPosts.route) { - BookmarkedPostsComposable(navController) - } - - composable(Destinations.FollowedHashtags.route) { - FollowedHashtagsComposable(navController) - } - - composable(Destinations.AboutInstance.route) { - AboutInstanceComposable(navController) - } - - composable(Destinations.AboutPixelix.route) { - AboutPixelixComposable(navController) - } - - composable(Destinations.OwnProfile.route) { - OwnProfileComposable(navController, openPreferencesDrawer) - } - - composable(Destinations.Followers.route) { navBackStackEntry -> - /*val uId = navBackStackEntry.arguments?.read { - if (hasValue("userid")) getString("userid") else null - } - val page = navBackStackEntry.arguments?.read { - if (hasValue("page")) getString("page") else null - }*/ - val uId = navBackStackEntry.arguments?.getString("userid") - val page = navBackStackEntry.arguments?.getString("page") - if (uId != null && page != null) { - FollowersMainComposable(navController, accountId = uId, page = page) - } - } - - composable( - "${Destinations.SinglePost.route}?refresh={refresh}&openReplies={openReplies}", - arguments = listOf(navArgument("refresh") { - defaultValue = false - }, navArgument("openReplies") { - defaultValue = false - }) - ) { navBackStackEntry -> - /*val uId = navBackStackEntry.arguments?.read { - if (hasValue("postid")) getString("postid") else null - } - val refresh = navBackStackEntry.arguments?.read { getBoolean("refresh") }!! - val openReplies = navBackStackEntry.arguments?.read { getBoolean("openReplies") }!! - */ - val uId = navBackStackEntry.arguments?.getString("postid") - val refresh = navBackStackEntry.arguments?.getBoolean("refresh")!! - val openReplies = navBackStackEntry.arguments?.getBoolean("openReplies")!! - uId?.let { id -> - SinglePostComposable(navController, postId = id, refresh, openReplies) - } - } - - composable(Destinations.Collection.route) { navBackStackEntry -> - /* val uId = navBackStackEntry.arguments?.read { - if (hasValue("collectionid")) getString("collectionid") else null - }*/ - val uId = navBackStackEntry.arguments?.getString("collectionid") - - uId?.let { id -> - CollectionComposable(navController, collectionId = id) - } - } - - composable(Destinations.Search.route) { navBackStackEntry -> - /*val initialPage = navBackStackEntry.arguments?.read { - if (hasValue("initialPage")) getInt("initialPage") else 0 - }*/ - val initialPage = navBackStackEntry.arguments?.getInt("initialPage") ?: 0 - - initialPage?.let { - ExploreComposable(navController, initialPage) - } - } - - composable(Destinations.Conversation.route) { - ConversationsComposable(navController = navController) - } - - composable(Destinations.Chat.route) { navBackStackEntry -> - /* val uId = navBackStackEntry.arguments?.read { - if (hasValue("userid")) getString("userid") else null - }*/ - val uId = navBackStackEntry.arguments?.getString("userid") - - uId?.let { id -> - ChatComposable(navController = navController, accountId = id) - } - } - - composable(Destinations.Mention.route) { navBackStackEntry -> - /* val mentionId = navBackStackEntry.arguments?.read { - if (hasValue("mentionid")) getString("mentionid") else null - }*/ - val mentionId = navBackStackEntry.arguments?.getString("mentionid") - - mentionId?.let { id -> - MentionComposable(navController = navController, mentionId = id) - } - } + Feeds( + Destination.HomeTabFeeds, + Res.drawable.house, + Res.drawable.house_fill, + Res.string.home + ), + Search( + Destination.HomeTabSearch, + Res.drawable.search_outline, + Res.drawable.search, + Res.string.search + ), + NewPost( + Destination.HomeTabNewPost, + Res.drawable.add_circle_outline, + Res.drawable.add_circle, + Res.string.new_post + ), + Notifications( + Destination.HomeTabNotifications, + Res.drawable.notifications_outline, + Res.drawable.notifications, + Res.string.notifications + ), + OwnProfile( + Destination.HomeTabOwnProfile, + Res.drawable.bookmark_outline, + Res.drawable.bookmark_outline, + Res.string.profile + ) } -//private fun SavedStateReader.hasValue(key: String) = contains(key) && !isNull(key) - @Composable private fun BottomBar( navController: NavHostController, openAccountSwitchBottomSheet: () -> Unit ) { - val screens = listOf( - Destinations.HomeScreen, - Destinations.Search, - Destinations.NewPost, - Destinations.NotificationsScreen, - Destinations.OwnProfile - ) val systemNavigationBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() @@ -478,10 +269,15 @@ private fun BottomBar( NavigationBar( modifier = Modifier.height(60.dp + systemNavigationBarHeight) ) { - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.destination?.route + val navBackStackEntry = navController.currentBackStackEntryAsState().value + val currentDestination = navBackStackEntry?.destination ?: return@NavigationBar + val tabContainer = currentDestination.parent ?: return@NavigationBar + + HomeTab.entries.forEach { tab -> + val isSelected = currentDestination.hierarchy.any { + it.hasRoute(tab.destination::class) + } - screens.forEach { screen -> val interactionSource = remember { MutableInteractionSource() } val coroutineScope = rememberCoroutineScope() var isLongPress by remember { mutableStateOf(false) } @@ -493,7 +289,7 @@ private fun BottomBar( isLongPress = false // Reset flag before starting detection coroutineScope.launch { delay(500L) // Long-press threshold - if (screen.route == Destinations.OwnProfile.route) { + if (tab == HomeTab.OwnProfile) { openAccountSwitchBottomSheet() } isLongPress = true @@ -506,40 +302,35 @@ private fun BottomBar( } } } - NavigationBarItem(icon = { - - - if (screen.route == Destinations.OwnProfile.route && avatar != null) { - Row(verticalAlignment = Alignment.CenterVertically) { - AsyncImage( - model = avatar, - error = painterResource(Res.drawable.default_avatar), - contentDescription = "", - modifier = Modifier - .height(30.dp) - .width(30.dp) - .clip(CircleShape) - ) + NavigationBarItem( + icon = { + if (tab == HomeTab.OwnProfile && avatar != null) { + Row(verticalAlignment = Alignment.CenterVertically) { + AsyncImage( + model = avatar, + error = painterResource(Res.drawable.default_avatar), + contentDescription = "", + modifier = Modifier + .height(30.dp) + .width(30.dp) + .clip(CircleShape) + ) + Icon( + Icons.Outlined.UnfoldMore, + contentDescription = "long press to switch account" + ) + } + } else { Icon( - Icons.Outlined.UnfoldMore, - contentDescription = "long press to switch account" + imageVector = vectorResource( + if (isSelected) tab.activeIcon else tab.icon + ), + modifier = Modifier.size(30.dp), + contentDescription = stringResource(tab.label) ) } - } else if (currentRoute?.startsWith(screen.route) == true) { - Icon( - imageVector = vectorResource(screen.activeIcon), - modifier = Modifier.size(30.dp), - contentDescription = stringResource(screen.label) - ) - } else { - Icon( - imageVector = vectorResource(screen.icon), - modifier = Modifier.size(30.dp), - contentDescription = stringResource(screen.label) - ) - } - }, - selected = currentRoute == screen.route, + }, + selected = isSelected, colors = NavigationBarItemDefaults.colors( selectedIconColor = MaterialTheme.colorScheme.inverseSurface, indicatorColor = Color.Transparent @@ -547,17 +338,38 @@ private fun BottomBar( interactionSource = interactionSource, onClick = { if (!isLongPress) { - Navigate.navigateWithPopUp(screen.route, navController) + if (!isSelected) { + //switch tab + navController.navigate(tab.destination) { + launchSingleTop = true + restoreState = true + popUpTo(tabContainer.route!!) { + inclusive = true + saveState = true + } + } + } else { + val tabRoot = tabContainer.findStartDestination() + val isOnRoot = currentDestination == tabRoot + if (!isOnRoot) { + //back to root + navController.popBackStack( + route = tabRoot.route!!, + inclusive = false + ) + } else if (currentDestination.hasRoute()) { + appComponent.searchFieldFocus.focus() + } + } } - }) + } + ) } } } -//https://partnerissuetracker.corp.google.com/issues/246909281 -@Composable -expect fun EdgeToEdgeDialog( - onDismissRequest: () -> Unit, - properties: DialogProperties, - content: @Composable () -> Unit -) +expect fun EdgeToEdgeDialogProperties( + dismissOnBackPress: Boolean = true, + dismissOnClickOutside: Boolean = false, + usePlatformDefaultWidth: Boolean = false +): DialogProperties 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 3e40a86d..60378d82 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt @@ -14,7 +14,10 @@ import com.daniebeler.pfpixelix.domain.model.SavedSearches 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.icon.AppIconManager import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences +import com.daniebeler.pfpixelix.domain.service.search.SearchFieldFocus import com.daniebeler.pfpixelix.domain.service.session.AuthService import com.daniebeler.pfpixelix.domain.service.session.Session import com.daniebeler.pfpixelix.domain.service.session.SessionStorage @@ -24,8 +27,6 @@ import com.daniebeler.pfpixelix.domain.service.share.SystemFileShare import com.daniebeler.pfpixelix.domain.service.widget.WidgetService import com.daniebeler.pfpixelix.utils.KmpContext import com.daniebeler.pfpixelix.utils.coilContext -import com.daniebeler.pfpixelix.utils.dataStoreDir -import com.daniebeler.pfpixelix.utils.imageCacheDir import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ExperimentalSettingsImplementation import com.russhwolf.settings.datastore.DataStoreSettings @@ -54,13 +55,17 @@ annotation class AppSingleton @AppSingleton @Component abstract class AppComponent( - @get:Provides val context: KmpContext + @get:Provides val context: KmpContext, + @get:Provides val fileService: FileService, + @get:Provides val iconManager: AppIconManager, ) { abstract val systemUrlHandler: SystemUrlHandler abstract val systemFileShare: SystemFileShare abstract val authService: AuthService abstract val widgetService: WidgetService + abstract val preferences: UserPreferences + abstract val searchFieldFocus: SearchFieldFocus @get:Provides @get:AppSingleton @@ -116,7 +121,7 @@ abstract class AppComponent( PreferenceDataStoreFactory.createWithPath( corruptionHandler = null, migrations = emptyList(), - produceFile = { context.dataStoreDir.resolve("settings.preferences_pb") }, + produceFile = { fileService.dataStoreDir.resolve("settings.preferences_pb") }, ) @Provides @@ -125,7 +130,7 @@ abstract class AppComponent( DataStoreFactory.create( storage = OkioStorage( fileSystem = FileSystem.SYSTEM, - producePath = { context.dataStoreDir.resolve("saved_searches.json") }, + producePath = { fileService.dataStoreDir.resolve("saved_searches.json") }, serializer = SavedSearchesSerializer, ) ) @@ -136,7 +141,7 @@ abstract class AppComponent( DataStoreFactory.create( storage = OkioStorage( fileSystem = FileSystem.SYSTEM, - producePath = { context.dataStoreDir.resolve("session_storage_datastore.json") }, + producePath = { fileService.dataStoreDir.resolve("session_storage_datastore.json") }, serializer = SessionStorageDataSerializer, ) ) @@ -160,7 +165,7 @@ abstract class AppComponent( .diskCache( DiskCache.Builder() .maxSizeBytes(50L * 1024L * 1024L) - .directory(context.imageCacheDir) + .directory(fileService.imageCacheDir) .build() ) .build() @@ -169,4 +174,8 @@ abstract class AppComponent( } @KmpComponentCreate -expect fun AppComponent.Companion.create(context: KmpContext): AppComponent \ No newline at end of file +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/model/AppAccentColor.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/AppAccentColor.kt new file mode 100644 index 00000000..05d3fdde --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/AppAccentColor.kt @@ -0,0 +1,8 @@ +package com.daniebeler.pfpixelix.domain.model + +object AppAccentColor { + const val GREEN = 0xFF2D6A44 + const val BLUE = 0xFF4C5C92 + const val RED = 0xFF8F4C38 + const val White = 0xFFFFFFFF +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/NewReport.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/NewReport.kt new file mode 100644 index 00000000..713b47eb --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/NewReport.kt @@ -0,0 +1,16 @@ +package com.daniebeler.pfpixelix.domain.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class NewReport ( + @SerialName("report_type") val reportType: String, + @SerialName("object_id") val objectId: String, + @SerialName("object_type") val objectType: ReportObjectType, +) + +enum class ReportObjectType { + @SerialName("post") POST, + @SerialName("user") User +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/ReportResponse.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/ReportResponse.kt new file mode 100644 index 00000000..1422eb91 --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/ReportResponse.kt @@ -0,0 +1,10 @@ +package com.daniebeler.pfpixelix.domain.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ReportResponse ( + @SerialName("msg") val message: String, + @SerialName("code") val code: Int +) \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/Server.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/Server.kt new file mode 100644 index 00000000..1f92860a --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/Server.kt @@ -0,0 +1,15 @@ +package com.daniebeler.pfpixelix.domain.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Server( + @SerialName("header_thumbnail") val headerThumbnail: String?, + @SerialName("domain") val domain: String, + @SerialName("mobile_registration") val mobileRegistrations: Boolean?, + @SerialName("version") val version: String, + @SerialName("short_description") val shortDescription: String?, + @SerialName("user_count") val userCount: Int, + @SerialName("last_seen_at") val lastSeenAt: String +) \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/TargetAccount.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/TargetAccount.kt new file mode 100644 index 00000000..831b9dc0 --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/TargetAccount.kt @@ -0,0 +1,3 @@ +package com.daniebeler.pfpixelix.domain.model + +class TargetAccount \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/repository/PixelfedApi.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/repository/PixelfedApi.kt index 62d08d54..b15df21d 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/repository/PixelfedApi.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/repository/PixelfedApi.kt @@ -16,7 +16,9 @@ import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.domain.model.PostContext import com.daniebeler.pfpixelix.domain.model.RelatedHashtag import com.daniebeler.pfpixelix.domain.model.Relationship +import com.daniebeler.pfpixelix.domain.model.ReportResponse import com.daniebeler.pfpixelix.domain.model.Search +import com.daniebeler.pfpixelix.domain.model.Server import com.daniebeler.pfpixelix.domain.model.Settings import com.daniebeler.pfpixelix.domain.model.Tag import de.jensklingenberg.ktorfit.Call @@ -25,6 +27,7 @@ import de.jensklingenberg.ktorfit.http.DELETE import de.jensklingenberg.ktorfit.http.Field import de.jensklingenberg.ktorfit.http.FormUrlEncoded import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.Header import de.jensklingenberg.ktorfit.http.Headers import de.jensklingenberg.ktorfit.http.POST import de.jensklingenberg.ktorfit.http.PUT @@ -230,8 +233,9 @@ interface PixelfedApi { @GET("api/v1.1/collections/items/{collectionid}") suspend fun getPostsOfCollection( - @Path("collectionid") collectionId: String - ): List + @Path("collectionid") collectionId: String, + @Query("page") page: Int, + ): List @POST("api/v1.1/collections/remove") suspend fun removePostOfCollection( @@ -355,6 +359,12 @@ interface PixelfedApi { @Path("id") postid: String ): Post + @Headers("Content-Type: application/json") + @POST("api/v1.1/report") + suspend fun reportPost( + @Body reportPostBody: String + ): ReportResponse + @GET("api/pixelfed/v1/web/settings") suspend fun getSettings(): Settings @@ -370,4 +380,9 @@ interface PixelfedApi { suspend fun getServerFromFediDB( @Path("slug") domain: String ): FediServerData + + @GET("https://pixelfed.org/api/v1/mobile-app/servers/open.json") + suspend fun getOpenServers( + @Header("X-Pixelfed-App") pixelfedApp: Int = 1 + ): List } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/collection/CollectionService.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/collection/CollectionService.kt index a8e59fed..b52e7bf2 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/collection/CollectionService.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/collection/CollectionService.kt @@ -18,8 +18,8 @@ class CollectionService( api.getCollection(collectionId) } - fun getPostsOfCollection(collectionId: String) = loadListResources { - api.getPostsOfCollection(collectionId) + fun getPostsOfCollection(collectionId: String, page: Int = 1) = loadListResources { + api.getPostsOfCollection(collectionId, page) } fun removePostOfCollection(collectionId: String, postId: String) = loadResource { 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 2dc37806..d72947f4 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 @@ -3,7 +3,7 @@ package com.daniebeler.pfpixelix.domain.service.editor 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.platform.Platform +import com.daniebeler.pfpixelix.domain.service.file.FileService import com.daniebeler.pfpixelix.domain.service.utils.loadResource import com.daniebeler.pfpixelix.utils.KmpUri import io.ktor.client.request.forms.MultiPartFormDataContent @@ -16,12 +16,12 @@ import me.tatarka.inject.annotations.Inject @Inject class PostEditorService( private val api: PixelfedApi, - private val platform: Platform, + private val fileService: FileService, private val json: Json ) { fun uploadMedia(uri: KmpUri, description: String) = loadResource { - val file = platform.getPlatformFile(uri) ?: error("File doesn't exist") + val file = fileService.getFile(uri) ?: error("File doesn't exist") val bytes = file.readBytes() val thumbnail = file.getThumbnail() 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 new file mode 100644 index 00000000..d08fb0cb --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/FileService.kt @@ -0,0 +1,23 @@ +package com.daniebeler.pfpixelix.domain.service.file + +import com.daniebeler.pfpixelix.utils.KmpUri +import okio.Path + +interface FileService { + val dataStoreDir: Path + val imageCacheDir: Path + + fun getFile(uri: KmpUri): PlatformFile? + fun downloadFile(name: String?, url: String) + fun getCacheSizeInBytes(): Long + fun cleanCache() +} + +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 diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AppIconService.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AppIconService.kt index 4efb34f4..685f889b 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AppIconService.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/AppIconService.kt @@ -1,21 +1,26 @@ package com.daniebeler.pfpixelix.domain.service.icon import com.daniebeler.pfpixelix.di.AppSingleton -import com.daniebeler.pfpixelix.domain.service.platform.Platform import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import me.tatarka.inject.annotations.Inject import org.jetbrains.compose.resources.DrawableResource import pixelix.app.generated.resources.Res -import pixelix.app.generated.resources.* +import pixelix.app.generated.resources.app_icon_00 +import pixelix.app.generated.resources.app_icon_01 +import pixelix.app.generated.resources.app_icon_02 +import pixelix.app.generated.resources.app_icon_03 +import pixelix.app.generated.resources.app_icon_05 +import pixelix.app.generated.resources.app_icon_06 +import pixelix.app.generated.resources.app_icon_07 +import pixelix.app.generated.resources.app_icon_08 +import pixelix.app.generated.resources.app_icon_09 @AppSingleton @Inject class AppIconService( - platform: Platform + private val iconManager: AppIconManager ) { - private val iconManager = platform.getAppIconManager() - val icons = listOf( Res.drawable.app_icon_00, Res.drawable.app_icon_01, @@ -35,4 +40,9 @@ class AppIconService( iconManager.setCustomIcon(icon) currentIconFlow.value = icon } +} + +interface AppIconManager { + fun getCurrentIcon(): DrawableResource + fun setCustomIcon(icon: DrawableResource) } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/instance/InstanceService.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/instance/InstanceService.kt index 97b9d455..b713ccec 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/instance/InstanceService.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/instance/InstanceService.kt @@ -22,6 +22,10 @@ class InstanceService( } fun getServerFromFediDB(slug: String) = loadResource { - api.getServerFromFediDB(slug).data + api.getServerFromFediDB(domain = slug).data + } + + fun getOpenServers() = loadResource { + api.getOpenServers() } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.kt index 5931b79e..ef4ff335 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.kt @@ -2,37 +2,16 @@ package com.daniebeler.pfpixelix.domain.service.platform import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.KmpUri import me.tatarka.inject.annotations.Inject -import org.jetbrains.compose.resources.DrawableResource @Inject expect class Platform( context: KmpContext, prefs: UserPreferences ) { - fun getPlatformFile(uri: KmpUri): PlatformFile? - fun getAppIconManager(): AppIconManager fun openUrl(url: String) + fun dismissBrowser() fun shareText(text: String) fun getAppVersion(): String fun pinWidget() - - fun downloadImageToGallery(name: String?, url: String) - - fun getCacheSizeInBytes(): Long - fun cleanCache() -} - -interface PlatformFile { - fun getName(): String - fun getSize(): Long - fun getMimeType(): String - suspend fun readBytes(): ByteArray - suspend fun getThumbnail(): ByteArray? -} - -interface AppIconManager { - fun getCurrentIcon(): DrawableResource - fun setCustomIcon(icon: DrawableResource) } 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 9b1cfcaa..53abb5dd 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 @@ -4,4 +4,8 @@ expect object PlatformFeatures { val notificationWidgets: Boolean 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/domain/service/post/PostService.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/post/PostService.kt index 15335ad3..f31be155 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/post/PostService.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/post/PostService.kt @@ -2,6 +2,7 @@ package com.daniebeler.pfpixelix.domain.service.post import com.daniebeler.pfpixelix.domain.model.LikedPostsWithNext import com.daniebeler.pfpixelix.domain.model.NewReply +import com.daniebeler.pfpixelix.domain.model.NewReport import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.domain.repository.PixelfedApi import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences @@ -29,7 +30,6 @@ class PostService( private val authService: AuthService, private val json: Json ) { - fun getPostById(postId: String) = loadResource { api.getPostById(postId) } @@ -110,6 +110,10 @@ class PostService( api.getBookmarkedPosts() } + fun reportPost(reportBody: NewReport) = loadResource { + api.reportPost(json.encodeToString(reportBody)) + } + fun getTrendingPosts(range: String) = loadListResources { api.getTrendingPosts(range) }.filterSensitive() diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/preferences/UserPreferences.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/preferences/UserPreferences.kt index a52690ef..14578db5 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/preferences/UserPreferences.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/preferences/UserPreferences.kt @@ -1,5 +1,6 @@ package com.daniebeler.pfpixelix.domain.service.preferences +import com.daniebeler.pfpixelix.domain.model.AppAccentColor import com.daniebeler.pfpixelix.domain.model.AppThemeMode import com.russhwolf.settings.ExperimentalSettingsApi import com.russhwolf.settings.ExperimentalSettingsImplementation @@ -7,6 +8,8 @@ import com.russhwolf.settings.boolean import com.russhwolf.settings.coroutines.toBlockingSettings import com.russhwolf.settings.datastore.DataStoreSettings import com.russhwolf.settings.int +import com.russhwolf.settings.long +import com.russhwolf.settings.string import me.tatarka.inject.annotations.Inject @OptIn(ExperimentalSettingsApi::class, ExperimentalSettingsImplementation::class) @@ -15,8 +18,10 @@ class UserPreferences(observableSettings: DataStoreSettings) { private val settings = observableSettings.toBlockingSettings() var hideSensitiveContent by settings.boolean("k_hide_sensitive_content", true) - var useInAppBrowser by settings.boolean("k_use_in_app_browser", true) + var blurSensitiveContent by settings.boolean("k_blur_sensitive_content", true) + var blurSensitiveContentFlow = observableSettings.getBooleanFlow("k_blur_sensitive_content", blurSensitiveContent) + var useInAppBrowser by settings.boolean("k_use_in_app_browser", true) var hideAltTextButton by settings.boolean("k_hide_alt_text_button", false) val hideAltTextButtonFlow = observableSettings.getBooleanFlow("k_hide_alt_text_button", hideAltTextButton) @@ -24,6 +29,10 @@ class UserPreferences(observableSettings: DataStoreSettings) { var focusMode by settings.boolean("k_focus_mode", false) val focusModeFlow = observableSettings.getBooleanFlow("k_focus_mode", focusMode) + var autoplayVideo by settings.boolean("k_autoplay_mode", true) + val autoplayVideoFlow = observableSettings.getBooleanFlow("k_autoplay_mode", autoplayVideo) + + var showUserGridTimeline by settings.boolean("k_grid_timeline", true) val showUserGridTimelineFlow = observableSettings.getBooleanFlow("k_grid_timeline", showUserGridTimeline) @@ -32,4 +41,7 @@ class UserPreferences(observableSettings: DataStoreSettings) { var appThemeMode by settings.int("k_theme_mode", AppThemeMode.FOLLOW_SYSTEM) val appThemeModeFlow = observableSettings.getIntFlow("k_theme_mode", appThemeMode) + + var accentColor by settings.long("k_accent_color", AppAccentColor.GREEN) + val accentColorFlow = observableSettings.getLongFlow("k_accent_color", accentColor) } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/search/SavedSearchesService.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/search/SavedSearchesService.kt index 8fe03b1b..c6f1a6e2 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/search/SavedSearchesService.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/search/SavedSearchesService.kt @@ -1,6 +1,7 @@ package com.daniebeler.pfpixelix.domain.service.search import androidx.datastore.core.DataStore +import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.domain.model.Account import com.daniebeler.pfpixelix.domain.model.SavedSearchItem import com.daniebeler.pfpixelix.domain.model.SavedSearchType @@ -38,7 +39,7 @@ class SavedSearchesService( } } } catch (e: Exception) { - println(e) + Logger.e("Add item error", e) } } @@ -50,7 +51,7 @@ class SavedSearchesService( ) } } catch (e: Exception) { - println(e) + Logger.e("deleteElement error", e) } } @@ -59,7 +60,7 @@ class SavedSearchesService( try { dataStore.updateData { SavedSearches() } } catch (e: Exception) { - println(e) + Logger.e("clearSavedSearches error", e) } } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/search/SearchFieldFocus.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/search/SearchFieldFocus.kt new file mode 100644 index 00000000..479f043c --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/search/SearchFieldFocus.kt @@ -0,0 +1,19 @@ +package com.daniebeler.pfpixelix.domain.service.search + +import com.daniebeler.pfpixelix.di.AppSingleton +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import me.tatarka.inject.annotations.Inject + +@AppSingleton +@Inject +class SearchFieldFocus { + private val eventsFlow = MutableSharedFlow() + val events = eventsFlow.asSharedFlow() + + fun focus() { + GlobalScope.launch { eventsFlow.emit(true) } + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthApi.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthApi.kt index f6310eb3..b28b97c4 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthApi.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthApi.kt @@ -1,6 +1,7 @@ package com.daniebeler.pfpixelix.domain.service.session import com.daniebeler.pfpixelix.domain.model.Account +import com.daniebeler.pfpixelix.domain.model.Server import de.jensklingenberg.ktorfit.http.Field import de.jensklingenberg.ktorfit.http.FormUrlEncoded import de.jensklingenberg.ktorfit.http.GET diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthService.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthService.kt index 8f68a7be..b3265735 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthService.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthService.kt @@ -3,6 +3,7 @@ package com.daniebeler.pfpixelix.domain.service.session import androidx.datastore.core.DataStore import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.di.AppSingleton +import com.daniebeler.pfpixelix.domain.service.platform.Platform import com.daniebeler.pfpixelix.domain.service.search.SavedSearchesService import de.jensklingenberg.ktorfit.Ktorfit import io.ktor.client.HttpClient @@ -25,7 +26,8 @@ class AuthService( private val session: Session, private val sessionStorage: DataStore, private val savedSearchesService: SavedSearchesService, - private val json: Json + private val json: Json, + private val platform: Platform ) { companion object { private const val clientName = "pixelix" @@ -50,9 +52,11 @@ class AuthService( } }.build() - urlHandler.openBrowser(authUrl.toString()) + // urlHandler.openBrowser(authUrl.toString()) + platform.openUrl(authUrl.toString()) val redirect = Url(urlHandler.redirects.first()) + platform.dismissBrowser() val code = redirect.parameters["code"] ?: error("Redirect doesn't have a code") @@ -92,6 +96,7 @@ class AuthService( } session.setCredentials(cred) data.copy(activeUserId = cred?.accountId) + } } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/Session.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/Session.kt index 37b69cae..da2f6804 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/Session.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/Session.kt @@ -27,8 +27,10 @@ class Session { suspend fun Sender.intercept(request: HttpRequestBuilder): HttpClientCall { credentials.value?.let { creds -> request.apply { - url.set(host = Url(creds.serverUrl).host) - headers["Authorization"] = "Bearer ${creds.token}" + if (url.host != "api.fedidb.org" && url.host != "pixelfed.org") { + url.set(host = Url(creds.serverUrl).host) + headers["Authorization"] = "Bearer ${creds.token}" + } } } return execute(request) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/CustomHashtag.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/CustomHashtag.kt index 99f634c8..db16cf89 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/CustomHashtag.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/CustomHashtag.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.daniebeler.pfpixelix.domain.model.Tag -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination @Composable fun CustomHashtag(hashtag: Tag, navController: NavController) { @@ -43,7 +43,7 @@ private fun CustomHashtagPrivate(hashtag: Tag, onClick: () -> Unit, navControlle .fillMaxWidth() .clickable { onClick() - Navigate.navigate("hashtag_timeline_screen/${hashtag.name}", navController) + navController.navigate(Destination.HashtagTimeline(hashtag.name)) }, verticalAlignment = Alignment.CenterVertically ) { Box( @@ -64,18 +64,11 @@ private fun CustomHashtagPrivate(hashtag: Tag, onClick: () -> Unit, navControlle Column { Text(text = "#" + hashtag.name) - if (hashtag.count != null) { - Text( - text = hashtag.count.toString() + " posts", - fontSize = 14.sp, - color = MaterialTheme.colorScheme.primary - ) - } else {/*Text( - text = hashtag.total.toString() + " people are talking", - fontSize = 14.sp, - color = MaterialTheme.colorScheme.primary - )*/ - } + Text( + text = hashtag.count.toString() + " posts", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.primary + ) } } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/CustomPost.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/CustomPost.kt index a6e1008c..e8d79928 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/CustomPost.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/CustomPost.kt @@ -13,17 +13,25 @@ import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import coil3.compose.AsyncImage +import com.daniebeler.pfpixelix.di.LocalAppComponent import com.daniebeler.pfpixelix.domain.model.Post +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.BlurHashDecoder -import com.daniebeler.pfpixelix.utils.Navigate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.stack @@ -38,14 +46,19 @@ fun CustomPost( edit: Boolean = false, editRemove: (postId: String) -> Unit = {} ) { + val prefs = LocalAppComponent.current.preferences + val blurSensitiveContent = remember { mutableStateOf(prefs.hideSensitiveContent) } - val blurHashBitmap = BlurHashDecoder.decode( - if (post.mediaAttachments.isNotEmpty()) { - post.mediaAttachments[0].blurHash ?: "LEHLk~WB2yk8pyo0adR*.7kCMdnj" - } else { - "LEHLk~WB2yk8pyo0adR*.7kCMdnj" - }, - ) + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(Unit) { + coroutineScope.launch { + prefs.blurSensitiveContentFlow.collect { blurSensitiveContent.value = it } + } + } + + val firstBlurHash = post.mediaAttachments.firstOrNull()?.blurHash + ?: "LEHLk~WB2yk8pyo0adR*.7kCMdnj" + val blurHashBitmap = remember(firstBlurHash) { BlurHashDecoder.decode(firstBlurHash) } Box(modifier = customModifier.aspectRatio(1f)) { if (blurHashBitmap != null) { @@ -57,14 +70,14 @@ fun CustomPost( ) } - if (post.sensitive) { + if (post.sensitive && blurSensitiveContent.value) { Box( contentAlignment = Alignment.Center, modifier = Modifier .aspectRatio(1f) .clickable(onClick = { if (!edit && onClick == null) { - Navigate.navigate("single_post_screen/" + post.id, navController) + navController.navigate(Destination.Post(post.id)) } else if (onClick != null){ onClick(post.id) } @@ -83,7 +96,7 @@ fun CustomPost( customModifier .clickable(onClick = { if (!edit && onClick == null) { - Navigate.navigate("single_post_screen/" + post.id, navController) + navController.navigate(Destination.Post(post.id)) } else if (onClick != null){ onClick(post.id) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/HomeComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/HomeComposable.kt index 707ac8fa..cea13c2c 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/HomeComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/HomeComposable.kt @@ -6,13 +6,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -41,7 +38,7 @@ import androidx.navigation.NavController import com.daniebeler.pfpixelix.ui.composables.timelines.global_timeline.GlobalTimelineComposable import com.daniebeler.pfpixelix.ui.composables.timelines.home_timeline.HomeTimelineComposable import com.daniebeler.pfpixelix.ui.composables.timelines.local_timeline.LocalTimelineComposable -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource @@ -81,10 +78,7 @@ fun HomeComposable(navController: NavController, openPreferencesDrawer: () -> Un Row { IconButton(onClick = { - Navigate.navigate( - "conversations", - navController - ) + navController.navigate(Destination.Conversations) }) { Icon( imageVector = vectorResource(Res.drawable.mail_outline), @@ -168,8 +162,7 @@ fun HomeComposable(navController: NavController, openPreferencesDrawer: () -> Un onDismissRequest = { showBottomSheet = false }, - sheetState = sheetState, - modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars) + sheetState = sheetState ) { Box( modifier = Modifier diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/InfiniteListHandler.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/InfiniteListHandler.kt index 1c665b6b..c665f582 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/InfiniteListHandler.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/InfiniteListHandler.kt @@ -49,7 +49,11 @@ fun InfiniteGridHandler( val totalItems = layoutInfo.totalItemsCount val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - lastVisibleItemIndex > (totalItems - buffer) + if (totalItems != 0) { + lastVisibleItemIndex > (totalItems - buffer) + } else { + false + } } } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/collection/CollectionComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/collection/CollectionComposable.kt index dc244a2c..feeb3ccb 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/collection/CollectionComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/collection/CollectionComposable.kt @@ -48,7 +48,6 @@ import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.ui.composables.ButtonRowElement import com.daniebeler.pfpixelix.ui.composables.InfinitePostsGrid import com.daniebeler.pfpixelix.ui.composables.states.EmptyState -import com.daniebeler.pfpixelix.utils.LocalKmpContext import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res @@ -74,8 +73,6 @@ fun CollectionComposable( var showBottomSheet by remember { mutableStateOf(false) } var showAddPostBottomSheet by remember { mutableStateOf(false) } - val context = LocalKmpContext.current - LaunchedEffect(Unit) { viewModel.loadData(collectionId) } @@ -132,13 +129,18 @@ fun CollectionComposable( } } else { - IconButton(onClick = { - viewModel.toggleEditMode() - }) { - Icon( - imageVector = Icons.Outlined.Edit, contentDescription = "" - ) + viewModel.collectionState.collection?.let { + if (it.username == viewModel.myUsername) { + IconButton(onClick = { + viewModel.toggleEditMode() + }) { + Icon( + imageVector = Icons.Outlined.Edit, contentDescription = "" + ) + } + } } + IconButton(onClick = { //Navigate.navigate("settings_screen", navController) showBottomSheet = true @@ -168,7 +170,7 @@ fun CollectionComposable( ), navController = navController, getItemsPaginated = { - //viewModel.getItemsPaginated() + viewModel.getPostsPaginated(false) }, after = { if (viewModel.editState.editMode) { @@ -198,8 +200,7 @@ fun CollectionComposable( onDismissRequest = { showBottomSheet = false }, - sheetState = sheetState, - modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars) + sheetState = sheetState ) { Column( modifier = Modifier.padding(bottom = 32.dp) @@ -209,9 +210,7 @@ fun CollectionComposable( Res.string.open_in_browser ), onClick = { if (viewModel.collectionState.collection != null) { - viewModel.openUrl( - viewModel.collectionState.collection!!.url, context - ) + viewModel.openUrl(viewModel.collectionState.collection!!.url) } }) @@ -229,8 +228,7 @@ fun CollectionComposable( onDismissRequest = { showAddPostBottomSheet = false }, - sheetState = showAddPostBottomSheetState, - modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars) + sheetState = showAddPostBottomSheetState ) { Column( modifier = Modifier.padding(bottom = 32.dp) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/collection/CollectionViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/collection/CollectionViewModel.kt index a132a85c..542fdfaf 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/collection/CollectionViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/collection/CollectionViewModel.kt @@ -5,11 +5,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.daniebeler.pfpixelix.domain.service.utils.Resource +import co.touchlab.kermit.Logger +import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.domain.service.collection.CollectionService import com.daniebeler.pfpixelix.domain.service.platform.Platform import com.daniebeler.pfpixelix.domain.service.post.PostService -import com.daniebeler.pfpixelix.utils.KmpContext +import com.daniebeler.pfpixelix.domain.service.session.AuthService +import com.daniebeler.pfpixelix.domain.service.utils.Resource import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject @@ -17,15 +19,19 @@ import me.tatarka.inject.annotations.Inject class CollectionViewModel @Inject constructor( private val platform: Platform, private val collectionService: CollectionService, - private val postService: PostService + private val postService: PostService, + private val authService: AuthService ) : ViewModel() { var collectionState by mutableStateOf(CollectionState()) var collectionPostsState by mutableStateOf(CollectionPostsState()) var editState by mutableStateOf(EditCollectionState()) + var myUsername: String? = null + var page: Int = 1 fun loadData(collectionId: String) { if (collectionState.id == null) { + myUsername = authService.getCurrentSession()!!.username collectionState = collectionState.copy(id = collectionId) getCollection() getPostsFirstLoad(false) @@ -64,12 +70,53 @@ class CollectionViewModel @Inject constructor( private fun getPostsFirstLoad(refreshing: Boolean) { if (collectionState.id != null) { - collectionService.getPostsOfCollection(collectionState.id!!).onEach { result -> + collectionService.getPostsOfCollection(collectionState.id!!, 1).onEach { result -> + when (result) { + is Resource.Success -> { + val endReached = (result.data?.size ?: 0) == 0 + collectionPostsState = CollectionPostsState( + posts = result.data ?: emptyList(), endReached = endReached + ) + getPostsPaginated(false) + } + + is Resource.Error -> { + collectionPostsState = CollectionPostsState( + error = result.message ?: "An unexpected error occurred" + ) + } + + is Resource.Loading -> { + collectionPostsState = CollectionPostsState( + isLoading = true, + isRefreshing = refreshing, + posts = collectionPostsState.posts + ) + } + } + }.launchIn(viewModelScope) + } + } + + fun getPostsPaginated(refreshing: Boolean) { + if (collectionState.id != null) { + if (collectionPostsState.posts.isEmpty()) { + return + } + page += 1 + + collectionService.getPostsOfCollection( + collectionState.id!!, + page + ).onEach { result -> collectionPostsState = when (result) { is Resource.Success -> { val endReached = (result.data?.size ?: 0) == 0 + var newPosts: List = result.data + newPosts = newPosts.drop(1); CollectionPostsState( - posts = result.data ?: emptyList(), endReached = endReached + posts = collectionPostsState.posts + newPosts, + endReached = endReached ) } @@ -95,7 +142,7 @@ class CollectionViewModel @Inject constructor( postService.getOwnPosts().onEach { result -> when (result) { is Resource.Success -> { - val posts = result.data!!.filter {!editState.editPosts.contains(it)} + val posts = result.data!!.filter { !editState.editPosts.contains(it) } editState = editState.copy(allPostsExceptCollection = posts) } @@ -112,10 +159,14 @@ class CollectionViewModel @Inject constructor( fun addPostToCollection(id: String) { val postToAdd = editState.allPostsExceptCollection.find { it.id == id } - val allPosts = editState.allPostsExceptCollection.filter {it.id != id} + val allPosts = editState.allPostsExceptCollection.filter { it.id != id } postToAdd?.let { val posts = editState.editPosts + postToAdd - editState = editState.copy(editPosts = posts, addedIds = editState.addedIds + id, allPostsExceptCollection = allPosts) + editState = editState.copy( + editPosts = posts, + addedIds = editState.addedIds + id, + allPostsExceptCollection = allPosts + ) } } @@ -133,12 +184,18 @@ class CollectionViewModel @Inject constructor( if (editState.name != collectionState.collection!!.title) { updateCollection(editState.name) } - collectionState = collectionState.copy(collection = collectionState.collection!!.copy(title = editState.name)) + collectionState = + collectionState.copy(collection = collectionState.collection!!.copy(title = editState.name)) } private fun updateCollection(newName: String) { if (collectionState.id != null && collectionState.collection != null) { - collectionService.updateCollection(collectionState.id!!, newName, collectionState.collection!!.description, collectionState.collection!!.visibility).onEach { result -> + collectionService.updateCollection( + collectionState.id!!, + newName, + collectionState.collection!!.description, + collectionState.collection!!.visibility + ).onEach { result -> when (result) { is Resource.Success -> { getCollection() @@ -178,21 +235,22 @@ class CollectionViewModel @Inject constructor( private fun removePostOfCollection(postId: String) { if (collectionState.id != null) { - collectionService.removePostOfCollection(collectionState.id!!, postId).onEach { result -> - when (result) { - is Resource.Success -> { - getPostsFirstLoad(false) - } + collectionService.removePostOfCollection(collectionState.id!!, postId) + .onEach { result -> + when (result) { + is Resource.Success -> { + getPostsFirstLoad(false) + } - is Resource.Error -> { + is Resource.Error -> { - } + } - is Resource.Loading -> { + is Resource.Loading -> { + } } - } - }.launchIn(viewModelScope) + }.launchIn(viewModelScope) } } @@ -217,10 +275,11 @@ class CollectionViewModel @Inject constructor( } fun refresh() { + page = 1 getPostsFirstLoad(true) } - fun openUrl(url: String, context: KmpContext) { + fun openUrl(url: String) { platform.openUrl(url) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/custom_account/CustomAccount.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/custom_account/CustomAccount.kt index 6f5f8972..ccd1cfce 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/custom_account/CustomAccount.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/custom_account/CustomAccount.kt @@ -29,13 +29,15 @@ import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.Account import com.daniebeler.pfpixelix.domain.model.Relationship import com.daniebeler.pfpixelix.ui.composables.FollowButton -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.StringFormat import org.jetbrains.compose.resources.painterResource -import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.resources.pluralStringResource +import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.default_avatar -import pixelix.app.generated.resources.followers +import pixelix.app.generated.resources.follower +import pixelix.app.generated.resources.trash @Composable fun CustomAccount( @@ -123,7 +125,7 @@ private fun CustomAccountPrivate( Row(modifier = Modifier .clickable { onClick() - Navigate.navigate("profile_screen/" + account.id, navController) + navController.navigate(Destination.Profile(account.id)) } .padding(horizontal = 12.dp, vertical = 8.dp) .fillMaxWidth(), @@ -153,7 +155,7 @@ private fun CustomAccountPrivate( Text( text = " • " + StringFormat.groupDigits( account.followersCount - ) + " " + stringResource(Res.string.followers), + ) + " " + pluralStringResource(Res.plurals.follower, account.followersCount), fontSize = 12.sp, color = MaterialTheme.colorScheme.primary, lineHeight = 8.sp @@ -238,7 +240,7 @@ private fun CustomAccountPrivateNotClickable( Text( text = " • " + StringFormat.groupDigits( account.followersCount - ) + " " + stringResource(Res.string.followers), + ) + " " + pluralStringResource(Res.plurals.follower, account.followersCount), fontSize = 12.sp, color = MaterialTheme.colorScheme.primary, lineHeight = 8.sp @@ -268,7 +270,7 @@ private fun CustomAccountPrivateNotClickable( contentAlignment = Alignment.Center ) { Icon( - imageVector = Icons.Outlined.Delete, + imageVector = vectorResource(Res.drawable.trash), contentDescription = null, tint = MaterialTheme.colorScheme.error ) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatComposable.kt index 9408fbbf..5ea37bd9 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatComposable.kt @@ -53,8 +53,7 @@ import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.ui.composables.InfiniteListHandler import com.daniebeler.pfpixelix.ui.composables.states.EndOfListComposable import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposable -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.imeAwareInsets import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -63,6 +62,7 @@ import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.beginning_of_chat_note import pixelix.app.generated.resources.chevron_back_outline import pixelix.app.generated.resources.default_avatar +import pixelix.app.generated.resources.message @OptIn(ExperimentalMaterial3Api::class) @@ -73,7 +73,6 @@ fun ChatComposable( viewModel: ChatViewModel = injectViewModel(key = "chat$accountId") { chatViewModel } ) { val lazyListState = rememberLazyListState() - val context = LocalKmpContext.current LaunchedEffect(Unit) { viewModel.getChat(accountId) } @@ -83,7 +82,7 @@ fun ChatComposable( if (viewModel.chatState.chat != null) { Row( modifier = Modifier.clickable { - Navigate.navigate("profile_screen/$accountId", navController) + navController.navigate(Destination.Profile(accountId)) }, verticalAlignment = Alignment.CenterVertically ) { AsyncImage( @@ -99,7 +98,7 @@ fun ChatComposable( Column { - Text(text = viewModel.chatState.chat!!.username ?: "") + Text(text = viewModel.chatState.chat!!.name ?: "") Text( text = viewModel.chatState.chat!!.url.substringAfter("https://") .substringBefore("/"), @@ -181,6 +180,7 @@ fun ChatComposable( Text( text = stringResource(Res.string.beginning_of_chat_note), textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.fillMaxWidth() ) } @@ -192,7 +192,7 @@ fun ChatComposable( Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Bottom) { OutlinedTextField(value = viewModel.newMessage, onValueChange = { viewModel.newMessage = it }, - label = { Text("Message") }, + label = { Text(stringResource(Res.string.message)) }, singleLine = false, colors = OutlinedTextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatElementComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatElementComposable.kt index 1133f862..8eece5ad 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatElementComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatElementComposable.kt @@ -33,7 +33,7 @@ import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.cancel import pixelix.app.generated.resources.delete import pixelix.app.generated.resources.this_action_cannot_be_undone - +import pixelix.app.generated.resources.delete_message @Composable fun ConversationElementComposable( message: Message, deleteMessage: () -> Unit, navController: NavController @@ -106,7 +106,7 @@ fun ConversationElementComposable( tint = MaterialTheme.colorScheme.error ) }, title = { - Text(text = "Delete message") + Text(text = stringResource(Res.string.delete_message)) }, text = { Text(text = stringResource(Res.string.this_action_cannot_be_undone)) }, onDismissRequest = { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatViewModel.kt index 1a819ff1..efff60c8 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/chat/ChatViewModel.kt @@ -5,10 +5,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.daniebeler.pfpixelix.domain.service.utils.Resource +import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.domain.model.Message import com.daniebeler.pfpixelix.domain.model.NewMessage import com.daniebeler.pfpixelix.domain.service.dm.DirectMessagesService +import com.daniebeler.pfpixelix.domain.service.utils.Resource import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject @@ -45,40 +46,44 @@ class ChatViewModel @Inject constructor( fun getChatPaginated(accountId: String) { if (chatState.chat != null && !chatState.isLoading && !chatState.endReached) { if (chatState.chat!!.messages.isNotEmpty()) { - directMessagesService.getChat(accountId, chatState.chat!!.messages.last().id).onEach { result -> - chatState = when (result) { - is Resource.Success -> { - val endReached = result.data?.messages!!.isEmpty() - - val existingMessageIds = chatState.chat?.messages?.map { it.id }?.toSet() ?: emptySet() - val newMessages = result.data.messages.filter { it.id !in existingMessageIds } - val messages = (chatState.chat?.messages ?: emptyList()) + newMessages - - val chat = chatState.chat?.copy() - if (chat != null) { - chat.messages = messages - ChatState( - chat = chat, - endReached = endReached - ) - } else { - ChatState( - chat = result.data - ) + directMessagesService.getChat(accountId, chatState.chat!!.messages.last().id) + .onEach { result -> + chatState = when (result) { + is Resource.Success -> { + val endReached = result.data?.messages!!.isEmpty() + + val existingMessageIds = + chatState.chat?.messages?.map { it.id }?.toSet() ?: emptySet() + val newMessages = + result.data.messages.filter { it.id !in existingMessageIds } + val messages = + (chatState.chat?.messages ?: emptyList()) + newMessages + + val chat = chatState.chat?.copy() + if (chat != null) { + chat.messages = messages + ChatState( + chat = chat, + endReached = endReached + ) + } else { + ChatState( + chat = result.data + ) + } } - } - is Resource.Error -> { - ChatState(error = result.message ?: "An unexpected error occurred") - } + is Resource.Error -> { + ChatState(error = result.message ?: "An unexpected error occurred") + } - is Resource.Loading -> { - ChatState( - isLoading = true, chat = chatState.chat - ) + is Resource.Loading -> { + ChatState( + isLoading = true, chat = chatState.chat + ) + } } - } - }.launchIn(viewModelScope) + }.launchIn(viewModelScope) } } } @@ -91,13 +96,11 @@ class ChatViewModel @Inject constructor( directMessagesService.sendMessage(newMsg).onEach { result -> newMessageState = when (result) { is Resource.Success -> { - if (result.data != null) { - val messages = emptyList() + result.data + chatState.chat!!.messages - val chat = chatState.chat?.copy() - if (chat != null) { - chat.messages = messages - chatState = ChatState(chat = chat) - } + val messages = emptyList() + result.data + chatState.chat!!.messages + val chat = chatState.chat?.copy() + if (chat != null) { + chat.messages = messages + chatState = ChatState(chat = chat) } NewMessageState(message = result.data) } @@ -120,22 +123,20 @@ class ChatViewModel @Inject constructor( directMessagesService.deleteMessage(id).onEach { result -> when (result) { is Resource.Success -> { - if (result.data != null) { - val messages = chatState.chat!!.messages.filter { it.reportId != id } - val chat = chatState.chat?.copy() - if (chat != null) { - chat.messages = messages - chatState = ChatState(chat = chat) - } + val messages = chatState.chat!!.messages.filter { it.reportId != id } + val chat = chatState.chat?.copy() + if (chat != null) { + chat.messages = messages + chatState = ChatState(chat = chat) } } is Resource.Error -> { - println(result.message) + Logger.e(result.message) } is Resource.Loading -> { - println("is loading") + Logger.v("is loading") } } }.launchIn(viewModelScope) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationElementComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationElementComposable.kt index 1e9f756a..b9916177 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationElementComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationElementComposable.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.domain.model.Conversation -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.painterResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.default_avatar @@ -33,7 +33,7 @@ fun ConversationElementComposable(conversation: Conversation, navController: Nav Modifier .fillMaxWidth() .clickable { - Navigate.navigate("chat/" + conversation.accounts.first().id, navController) + navController.navigate(Destination.Chat(conversation.accounts.first().id)) } .padding(horizontal = 12.dp, vertical = 14.dp), verticalAlignment = Alignment.CenterVertically) { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationsComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationsComposable.kt index 23639131..7a88c217 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationsComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationsComposable.kt @@ -56,7 +56,7 @@ import com.daniebeler.pfpixelix.ui.composables.states.EndOfListComposable import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposable import com.daniebeler.pfpixelix.ui.composables.states.FullscreenEmptyStateComposable import com.daniebeler.pfpixelix.ui.composables.states.LoadingComposable -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res @@ -176,8 +176,7 @@ fun ConversationsComposable( onDismissRequest = { showBottomSheet = false }, - sheetState = sheetState, - modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars) + sheetState = sheetState ) { Box( modifier = Modifier @@ -263,9 +262,7 @@ private fun CreateNewConversation( }, confirmButton = { TextButton(enabled = viewModel.newConversationSelectedAccount != null, onClick = { if (viewModel.newConversationSelectedAccount != null) { - Navigate.navigate( - "chat/" + viewModel.newConversationSelectedAccount!!.id, navController - ) + navController.navigate(Destination.Chat(viewModel.newConversationSelectedAccount!!.id)) close() } }) { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationsViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationsViewModel.kt index 0bcff754..5ac47853 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationsViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/direct_messages/conversations/ConversationsViewModel.kt @@ -23,6 +23,7 @@ class ConversationsViewModel @Inject constructor( var newConversationUsername by mutableStateOf(TextFieldValue()) var newConversationState by mutableStateOf(NewConversationState()) var newConversationSelectedAccount by mutableStateOf(null) + init { getConversationsFirstLoad(false) } @@ -58,13 +59,7 @@ class ConversationsViewModel @Inject constructor( searchService.search(newUsername.text, "accounts").onEach { result -> newConversationState = when (result) { is Resource.Success -> { - if (result.data != null) { - NewConversationState(suggestions = result.data.accounts) - } else { - NewConversationState( - error = result.message ?: "An unexpected error occurred" - ) - } + NewConversationState(suggestions = result.data.accounts) } is Resource.Error -> { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_post/EditPostComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_post/EditPostComposable.kt index 14d3aead..b96726ea 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_post/EditPostComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_post/EditPostComposable.kt @@ -1,6 +1,5 @@ package com.daniebeler.pfpixelix.ui.composables.edit_post -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,7 +11,6 @@ import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding @@ -32,8 +30,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowLeft import androidx.compose.material.icons.automirrored.outlined.ArrowRight -import androidx.compose.material.icons.outlined.ArrowDownward -import androidx.compose.material.icons.outlined.ArrowUpward import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -72,24 +68,17 @@ import androidx.navigation.NavController import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.MediaAttachment -import com.daniebeler.pfpixelix.ui.composables.newpost.ImagesPager -import com.daniebeler.pfpixelix.ui.composables.newpost.NewPostViewModel import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposable +import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposableDialog import com.daniebeler.pfpixelix.ui.composables.states.LoadingComposable import com.daniebeler.pfpixelix.ui.composables.textfield_location.TextFieldLocationsComposable import com.daniebeler.pfpixelix.ui.composables.textfield_mentions.TextFieldMentionsComposable -import com.daniebeler.pfpixelix.utils.KmpUri -import com.daniebeler.pfpixelix.utils.LocalKmpContext import com.daniebeler.pfpixelix.utils.getPlatformUriObject 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 kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res -import pixelix.app.generated.resources.add_outline import pixelix.app.generated.resources.alt_text import pixelix.app.generated.resources.cancel import pixelix.app.generated.resources.caption @@ -118,8 +107,6 @@ fun EditPostComposable( val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current - val context = LocalKmpContext.current - LaunchedEffect(Unit) { viewModel.loadData(postId) } @@ -335,7 +322,11 @@ fun EditPostComposable( } LoadingComposable(isLoading = viewModel.editPostState.isLoading) - ErrorComposable(message = viewModel.editPostState.error) + ErrorComposableDialog( + errorMessage = viewModel.editPostState.error, + onDismiss = { viewModel.editPostState = viewModel.editPostState.copy(error = "") } + ) + Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } @@ -469,7 +460,8 @@ fun ImagesPagerEditPost( AsyncImage( model = image.url.toKmpUri().getPlatformUriObject(), contentDescription = "video thumbnail", - modifier = Modifier.width(100.dp) + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.Inside ) } else { AsyncImage( diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_post/EditPostViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_post/EditPostViewModel.kt index 74af4b41..129b980a 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_post/EditPostViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/edit_post/EditPostViewModel.kt @@ -47,21 +47,19 @@ class EditPostViewModel @Inject constructor( postService.getPostById(postId).onEach { result -> editPostState = when (result) { is Resource.Success -> { - if (result.data != null) { - caption = TextFieldValue(result.data.content) - result.data.place?.let { - location = it - } - sensitive = result.data.sensitive - sensitiveText = result.data.spoilerText - mediaAttachmentsEdit.addAll(result.data.mediaAttachments) - mediaAttachmentsBefore.addAll(result.data.mediaAttachments) - mediaDescriptionItems.addAll(result.data.mediaAttachments.map { - MediaDescriptionItem( - it.id, it.description ?: "", false - ) - }) + caption = TextFieldValue(result.data.content) + result.data.place?.let { + location = it } + sensitive = result.data.sensitive + sensitiveText = result.data.spoilerText + mediaAttachmentsEdit.addAll(result.data.mediaAttachments) + mediaAttachmentsBefore.addAll(result.data.mediaAttachments) + mediaDescriptionItems.addAll(result.data.mediaAttachments.map { + MediaDescriptionItem( + it.id, it.description ?: "", false + ) + }) EditPostState(post = result.data) } 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 d9fde62f..cb4c14be 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 @@ -3,9 +3,7 @@ package com.daniebeler.pfpixelix.ui.composables.edit_profile import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -17,15 +15,12 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Surface -import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Done @@ -51,9 +46,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.decodeToImageBitmap import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.font.FontWeight @@ -62,31 +55,21 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavController import coil3.compose.AsyncImage -import com.attafitamim.krop.core.crop.CircleCropShape import com.attafitamim.krop.core.crop.CropResult -import com.attafitamim.krop.core.crop.CropShape import com.attafitamim.krop.core.crop.CropState -import com.attafitamim.krop.core.crop.CropperStyle import com.attafitamim.krop.core.crop.DefaultCropperStyle import com.attafitamim.krop.core.crop.LocalCropperStyle import com.attafitamim.krop.core.crop.rememberImageCropper import com.attafitamim.krop.core.images.ImageBitmapSrc -import com.attafitamim.krop.ui.CropperDialogProperties import com.attafitamim.krop.ui.CropperPreview import com.attafitamim.krop.ui.DefaultControls -import com.attafitamim.krop.ui.DefaultTopBar -import com.attafitamim.krop.ui.ImageCropperDialog -import com.daniebeler.pfpixelix.EdgeToEdgeDialog +import com.daniebeler.pfpixelix.EdgeToEdgeDialogProperties import com.daniebeler.pfpixelix.di.injectViewModel -import com.daniebeler.pfpixelix.ui.theme.PixelixTheme -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.getPlatformUriObject 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 kotlinx.coroutines.launch -import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res @@ -348,12 +331,9 @@ private fun ImageCropperFullscreenDialog( } CompositionLocalProvider(LocalCropperStyle provides style) { - EdgeToEdgeDialog( + Dialog( onDismissRequest = { state.done(accept = false) }, - properties = DialogProperties( - dismissOnClickOutside = false, - usePlatformDefaultWidth = false, - ) + properties = EdgeToEdgeDialogProperties() ) { Scaffold( contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top), diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/ExploreComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/ExploreComposable.kt index 8f72a962..745224c2 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/ExploreComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/ExploreComposable.kt @@ -46,18 +46,23 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import co.touchlab.kermit.Logger import coil3.compose.AsyncImage +import com.daniebeler.pfpixelix.di.LocalAppComponent import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.Account import com.daniebeler.pfpixelix.domain.model.SavedSearchItem @@ -66,9 +71,7 @@ import com.daniebeler.pfpixelix.ui.composables.CustomHashtag import com.daniebeler.pfpixelix.ui.composables.custom_account.CustomAccount import com.daniebeler.pfpixelix.ui.composables.explore.trending.TrendingComposable import com.daniebeler.pfpixelix.ui.composables.states.FullscreenLoadingComposable -import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.imeAwareInsets import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource @@ -88,13 +91,21 @@ fun ExploreComposable( initialPage: Int = 0, viewModel: ExploreViewModel = injectViewModel(key = "search-viewmodel-key") { exploreViewModel } ) { - val context: KmpContext = LocalKmpContext.current - + val focusRequester = remember { FocusRequester() } val textFieldState = rememberTextFieldState() var expanded by rememberSaveable { mutableStateOf(false) } + val appComponent = LocalAppComponent.current + LaunchedEffect(Unit) { + appComponent.searchFieldFocus.events.collect { + Logger.d("search") { "Request search focus" } + focusRequester.requestFocus() + } + } + Box(Modifier .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) .semantics { isTraversalGroup = true }) { SearchBar( modifier = Modifier @@ -112,6 +123,7 @@ fun ExploreComposable( expanded = expanded, onExpandedChange = { expanded = it }, placeholder = { Text(stringResource(Res.string.explore)) }, + modifier = Modifier.focusRequester(focusRequester), leadingIcon = { if (!expanded) { Icon(vectorResource(Res.drawable.search_outline), contentDescription = null) @@ -296,13 +308,9 @@ private fun PastSearchItem( .fillMaxWidth() .clickable { when (item.savedSearchType) { - SavedSearchType.Account -> Navigate.navigate( - "profile_screen/" + item.account!!.id, navController - ) + SavedSearchType.Account -> navController.navigate(Destination.Profile(item.account!!.id)) - SavedSearchType.Hashtag -> Navigate.navigate( - "hashtag_timeline_screen/${item.value}", navController - ) + SavedSearchType.Hashtag -> navController.navigate(Destination.HashtagTimeline(item.value)) SavedSearchType.Search -> setSearchText(item.value) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/TrendingComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/TrendingComposable.kt index d9c2197d..2e76b331 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/TrendingComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/TrendingComposable.kt @@ -125,8 +125,7 @@ fun TrendingComposable(navController: NavController, initialPage: Int) { onDismissRequest = { showBottomSheet = false }, - sheetState = sheetState, - modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars) + sheetState = sheetState ) { Box( modifier = Modifier diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_accounts/TrendingAccountElement.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_accounts/TrendingAccountElement.kt index bf22f3e1..55035291 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_accounts/TrendingAccountElement.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_accounts/TrendingAccountElement.kt @@ -20,8 +20,7 @@ import com.daniebeler.pfpixelix.domain.model.Account import com.daniebeler.pfpixelix.ui.composables.CustomPost import com.daniebeler.pfpixelix.ui.composables.custom_account.CustomAccount import com.daniebeler.pfpixelix.ui.composables.hashtagMentionText.HashtagsMentionsTextView -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination @Composable fun TrendingAccountElement( @@ -29,7 +28,6 @@ fun TrendingAccountElement( navController: NavController, viewModel: TrendingAccountElementViewModel = injectViewModel(key = account.id) { trendingAccountElementViewModel } ) { - val context = LocalKmpContext.current LaunchedEffect(account) { viewModel.loadItems(account.id) } @@ -39,7 +37,7 @@ fun TrendingAccountElement( .padding(12.dp) .fillMaxWidth() .clickable { - Navigate.navigate("profile_screen/" + account.id, navController) + navController.navigate(Destination.Profile(account.id)) }) { CustomAccount(account = account) @@ -49,7 +47,7 @@ fun TrendingAccountElement( text = account.note, mentions = null, navController = navController, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), openUrl = { url -> viewModel.openUrl(url, context) } + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), openUrl = { url -> viewModel.openUrl(url) } ) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_accounts/TrendingAccountElementViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_accounts/TrendingAccountElementViewModel.kt index 50ebf473..aafc3626 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_accounts/TrendingAccountElementViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_accounts/TrendingAccountElementViewModel.kt @@ -5,10 +5,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.daniebeler.pfpixelix.domain.service.utils.Resource import com.daniebeler.pfpixelix.domain.service.platform.Platform import com.daniebeler.pfpixelix.domain.service.post.PostService -import com.daniebeler.pfpixelix.utils.KmpContext +import com.daniebeler.pfpixelix.domain.service.utils.Resource import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject @@ -47,7 +46,7 @@ class TrendingAccountElementViewModel @Inject constructor( } } - fun openUrl(url: String, context: KmpContext) { + fun openUrl(url: String) { platform.openUrl(url) } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_hashtags/TrendingHashtagElement.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_hashtags/TrendingHashtagElement.kt index 0bdf21f7..8a1e85be 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_hashtags/TrendingHashtagElement.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/explore/trending/trending_hashtags/TrendingHashtagElement.kt @@ -29,7 +29,7 @@ import androidx.navigation.NavController import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.Tag import com.daniebeler.pfpixelix.ui.composables.CustomPost -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.StringFormat import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res @@ -52,7 +52,7 @@ fun TrendingHashtagElement( .padding(vertical = 8.dp) .fillMaxWidth() .clickable { - Navigate.navigate("hashtag_timeline_screen/${hashtag.name}", navController) + navController.navigate(Destination.HashtagTimeline(hashtag.name)) }) { Row( diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowerElementComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowerElementComposable.kt index f0d888c4..0be0c2e6 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowerElementComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowerElementComposable.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.domain.model.Account -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.painterResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.default_avatar @@ -32,7 +32,7 @@ fun FollowerElementComposable( Row( modifier = Modifier .clickable { - Navigate.navigate("profile_screen/" + account.id, navController, false) + navController.navigate(Destination.Profile(account.id)) } .padding(horizontal = 12.dp, vertical = 8.dp) .fillMaxWidth() diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowersMainComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowersMainComposable.kt index 0cdeec62..2b08cf37 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowersMainComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowersMainComposable.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.di.injectViewModel import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource @@ -47,7 +46,7 @@ import pixelix.app.generated.resources.following fun FollowersMainComposable( navController: NavController, accountId: String, - page: String, + isFollowers: Boolean, viewModel: FollowersViewModel = injectViewModel(key = "followers-viewmodel-key") { followersViewModel } ) { @@ -59,7 +58,7 @@ fun FollowersMainComposable( viewModel.setLoggedInAccountIdValue() } - val pageId = if (page == "followers") 0 else 1 + val pageId = if (isFollowers) 0 else 1 val pagerState = rememberPagerState(initialPage = pageId, pageCount = { 2 }) val scope = rememberCoroutineScope() diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowingComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowingComposable.kt index 364a2712..af02847d 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowingComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/followers/FollowingComposable.kt @@ -10,15 +10,11 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Groups import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.Key.Companion.R import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.ui.composables.InfiniteListHandler import com.daniebeler.pfpixelix.ui.composables.states.EmptyState @@ -26,7 +22,7 @@ import com.daniebeler.pfpixelix.ui.composables.states.EndOfListComposable import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposable import com.daniebeler.pfpixelix.ui.composables.states.FullscreenEmptyStateComposable import com.daniebeler.pfpixelix.ui.composables.states.LoadingComposable -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.empty @@ -80,7 +76,7 @@ fun FollowingComposable( message = message, buttonText = stringResource(Res.string.explore_trending_profiles), onClick = { - Navigate.navigate("search_screen/1", navController) + navController.navigate(Destination.Search(1)) }) ) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/hashtagMentionText/TextWithClickableHashtagsAndMentionsComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/hashtagMentionText/TextWithClickableHashtagsAndMentionsComposable.kt index 68471f41..c7243c23 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/hashtagMentionText/TextWithClickableHashtagsAndMentionsComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/hashtagMentionText/TextWithClickableHashtagsAndMentionsComposable.kt @@ -24,11 +24,14 @@ import androidx.compose.ui.unit.TextUnit import androidx.navigation.NavController import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.Account -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import org.jetbrains.compose.resources.stringResource +import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.read_less +import pixelix.app.generated.resources.read_more @Composable fun HashtagsMentionsTextView( @@ -127,42 +130,32 @@ fun HashtagsMentionsTextView( } }, modifier = modifier, onClick = { position -> - CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.Main).launch { val annotatedStringRange = annotatedStringList.firstOrNull { it.start <= position && position < it.end } if (annotatedStringRange != null) { if (annotatedStringRange.tag == "tag" || annotatedStringRange.tag == "account") { val newItem = annotatedStringRange.item.drop(1) - val route = if (annotatedStringRange.tag == "tag") { - "hashtag_timeline_screen/$newItem" + if (annotatedStringRange.tag == "tag") { + navController.navigate(Destination.HashtagTimeline(newItem)) } else { if (mentions == null) { - "profile_screen/byUsername/${annotatedStringRange.item.drop(1)}" + navController.navigate(Destination.ProfileByUsername(annotatedStringRange.item.drop(1))) } else { - var account = - mentions.find { account: Account -> account.acct == newItem } + var account = mentions.find { account: Account -> account.acct == newItem } if (account == null) { - account = - mentions.find { account: Account -> account.username == newItem } + account = mentions.find { account: Account -> account.username == newItem } } if (account != null) { //get my account id and check if it is mine account val myAccountId = viewModel.getMyAccountId() if (account.id == myAccountId) { - "own_profile_screen" + navController.navigate(Destination.OwnProfile) } else { - "profile_screen/${account.id}" + navController.navigate(Destination.Profile(account.id)) } - } else { - "" } } - - } - withContext(Dispatchers.Main) { - if (route.isNotBlank() && route.isNotEmpty()) { - Navigate.navigate(route, navController) - } } } else if (annotatedStringRange.tag == "link") { openUrl(annotatedStringRange.item) @@ -172,7 +165,7 @@ fun HashtagsMentionsTextView( }) if (showReadMoreButtonState) { Text( - text = if (expanded) "Read Less" else "Read More", + text = if (expanded) stringResource(Res.string.read_less) else stringResource(Res.string.read_more), color = Color.Gray, modifier = Modifier.clickable { expanded = !expanded diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/mention/MentionComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/mention/MentionComposable.kt index 084aab8f..fa4ea8f7 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/mention/MentionComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/mention/MentionComposable.kt @@ -36,13 +36,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.ui.composables.post.PostComposable import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposable import com.daniebeler.pfpixelix.ui.composables.states.LoadingComposable -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource @@ -115,7 +114,7 @@ fun MentionComposable( post, navController, postGetsDeleted = { - Navigate.navigateAndDeleteBackStack("own_profile_screen", navController) + navController.navigate(Destination.OwnProfile) }, setZindex = { }, openReplies = false, @@ -146,15 +145,15 @@ private fun SubPosts(posts: List, navController: NavController) { ancestor, navController, postGetsDeleted = { - Navigate.navigateAndDeleteBackStack( - "own_profile_screen", navController - ) + navController.navigate(Destination.OwnProfile) { + popUpTo(0) { inclusive = true } + } }, setZindex = { }, openReplies = false, showReplies = false, modifier = Modifier.clickable { - Navigate.navigate("mention/" + ancestor.id, navController) + navController.navigate(Destination.Mention(ancestor.id)) } ) } 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 7c91aa00..29e2d839 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 @@ -72,10 +72,10 @@ import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.Visibility import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposable +import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposableDialog import com.daniebeler.pfpixelix.ui.composables.states.LoadingComposable import com.daniebeler.pfpixelix.ui.composables.textfield_location.TextFieldLocationsComposable import com.daniebeler.pfpixelix.utils.KmpUri -import com.daniebeler.pfpixelix.utils.LocalKmpContext import com.daniebeler.pfpixelix.utils.getPlatformUriObject import com.daniebeler.pfpixelix.utils.imeAwareInsets import com.daniebeler.pfpixelix.utils.toKmpUri @@ -109,8 +109,6 @@ fun NewPostComposable( uris: List? = null, viewModel: NewPostViewModel = injectViewModel(key = "new-post-viewmodel-key") { newPostViewModel } ) { - val context = LocalKmpContext.current - var expanded by remember { mutableStateOf(false) } var showReleaseAlert by remember { mutableStateOf(false) @@ -118,9 +116,7 @@ fun NewPostComposable( LaunchedEffect(uris) { uris?.let { - uris.forEach { - viewModel.addImage(uri = it, context = context) - } + uris.forEach { viewModel.addImage(uri = it) } } } @@ -151,7 +147,7 @@ fun NewPostComposable( { index -> viewModel.moveMediaAttachmentUp(index) }, { index -> viewModel.moveMediaAttachmentDown(index) }, { index -> viewModel.deleteMedia(index) }, - { kmpUri: KmpUri -> viewModel.addImage(kmpUri, context) }) + { kmpUri: KmpUri -> viewModel.addImage(kmpUri) }) Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { NewPostTextField( @@ -282,8 +278,15 @@ fun NewPostComposable( LoadingComposable(isLoading = viewModel.createPostState.isLoading) //LoadingComposable(isLoading = viewModel.mediaUploadState.isLoading) - ErrorComposable(message = viewModel.mediaUploadState.error) - ErrorComposable(message = viewModel.createPostState.error) + ErrorComposableDialog( + errorMessage = viewModel.mediaUploadState.error, + onDismiss = { viewModel.mediaUploadState = viewModel.mediaUploadState.copy(error = "") } + ) + + ErrorComposableDialog( + errorMessage = viewModel.createPostState.error, + onDismiss = { viewModel.createPostState = viewModel.createPostState.copy(error = "") } + ) } } } @@ -392,7 +395,8 @@ fun ImagesPager( AsyncImage( model = image.imageUri.getPlatformUriObject(), contentDescription = "video thumbnail", - modifier = Modifier.width(100.dp) + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.Inside ) } else { AsyncImage( diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostPref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostPref.kt index 7ad8ee30..4e40bf5e 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostPref.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostPref.kt @@ -49,9 +49,7 @@ fun NewPostPref( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.defaultMinSize(minHeight = 54.dp) ) { - if (leadingIcon != null) { - Icon(vectorResource(leadingIcon), contentDescription = null) - } + Icon(vectorResource(leadingIcon), contentDescription = null) Column( modifier = Modifier .weight(1f) 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 26242b90..e8679d61 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 @@ -7,16 +7,16 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController -import com.daniebeler.pfpixelix.domain.service.utils.Resource import com.daniebeler.pfpixelix.domain.model.Instance 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.instance.InstanceService -import com.daniebeler.pfpixelix.domain.service.platform.Platform -import com.daniebeler.pfpixelix.utils.KmpContext +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 com.daniebeler.pfpixelix.utils.Navigate import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.flowOn @@ -28,7 +28,7 @@ import me.tatarka.inject.annotations.Inject class NewPostViewModel @Inject constructor( private val postEditorService: PostEditorService, private val instanceService: InstanceService, - private val platform: Platform + private val fileService: FileService ) : ViewModel() { data class ImageItem( val imageUri: KmpUri, @@ -57,13 +57,7 @@ class NewPostViewModel @Inject constructor( instanceService.getInstance().onEach { result -> when (result) { is Resource.Success -> { - if (result.data != null) { - instance = result.data - } else { - createPostState = CreatePostState( - error = result.message ?: "An unexpected error occurred" - ) - } + instance = result.data } is Resource.Error -> { @@ -101,8 +95,8 @@ class NewPostViewModel @Inject constructor( } } - fun addImage(uri: KmpUri, context: KmpContext) { - val file = platform.getPlatformFile(uri) ?: return + fun addImage(uri: KmpUri) { + val file = fileService.getFile(uri) ?: return val fileType = file.getMimeType() if (instance != null && !instance!!.configuration.mediaAttachmentConfig.supportedMimeTypes.contains( fileType @@ -141,11 +135,14 @@ class NewPostViewModel @Inject constructor( } val imagesNumber = images.size + 1 if (instance != null && imagesNumber > instance!!.configuration.statusConfig.maxMediaAttachments) { - addImageError = Pair("To many images", "You have added to many images, your Server does only allow ${instance!!.configuration.statusConfig.maxMediaAttachments} images per post") + addImageError = Pair( + "To many images", + "You have added to many images, your Server does only allow ${instance!!.configuration.statusConfig.maxMediaAttachments} images per post" + ) return } images += ImageItem(uri, fileType, null, "", true) - uploadImage(context, uri, "") + uploadImage(uri, "") } fun deleteMedia(index: Int) { @@ -168,7 +165,7 @@ class NewPostViewModel @Inject constructor( } } - private fun uploadImage(context: KmpContext, uri: KmpUri, text: String) { + private fun uploadImage(uri: KmpUri, text: String) { postEditorService.uploadMedia(uri, text).onEach { result -> mediaUploadState = when (result) { is Resource.Success -> { @@ -177,7 +174,10 @@ class NewPostViewModel @Inject constructor( } val index = images.indexOfFirst { it.imageUri == uri } if (index != -1) { - images[index] = images[index].copy(isLoading = false, id = result.data?.id) // Replacing the object forces recomposition + images[index] = images[index].copy( + isLoading = false, + id = result.data?.id + ) // Replacing the object forces recomposition } mediaUploadState.copy( @@ -187,11 +187,11 @@ class NewPostViewModel @Inject constructor( } is Resource.Error -> { - if (!result.message.isNullOrEmpty()) { - MediaUploadState(error = result.message) - } else { - MediaUploadState(error = "An unexpected error occured") + val index = images.indexOfFirst { it.imageUri == uri } + if (index != -1) { + images.removeAt(index) } + MediaUploadState(error = "An unexpected error occured") } is Resource.Loading -> { @@ -284,9 +284,7 @@ class NewPostViewModel @Inject constructor( postEditorService.createPost(createPostDto).onEach { result -> createPostState = when (result) { is Resource.Success -> { - if (result.data != null) { - Navigate.navigateAndDeleteBackStack("own_profile_screen", navController) - } + navController.navigate(Destination.OwnProfile) CreatePostState(post = result.data, isLoading = true) } 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 028a244e..710639eb 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 @@ -11,23 +11,30 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.Colors import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.produceState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.Notification -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.TimeAgo import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -78,63 +85,97 @@ fun CustomNotification( } } + val timeAgoText = produceState(initialValue = "") { + value = TimeAgo.convertTimeToText(notification.createdAt) + } + Row( - Modifier - .padding(horizontal = 12.dp, vertical = 8.dp) - .fillMaxWidth().clickable { - if (notification.post != null && notification.post.mediaAttachments.isEmpty()) { - Navigate.navigate("mention/" + notification.post.id, navController) - } - }, - verticalAlignment = Alignment.CenterVertically + Modifier.padding(horizontal = 12.dp, vertical = 8.dp).fillMaxWidth().clickable { + if (notification.post != null && notification.post.mediaAttachments.isEmpty()) { + navController.navigate(Destination.Mention(notification.post.id)) + } else if (notification.post != null && notification.post.mediaAttachments.isNotEmpty()) { + navController.navigate(Destination.Post(notification.post.id)) + } else if (notification.post == null) { + navController.navigate(Destination.Profile(notification.account.id)) + } + }, verticalAlignment = Alignment.CenterVertically ) { - AsyncImage(model = notification.account?.avatar, + AsyncImage( + model = notification.account.avatar, error = painterResource(Res.drawable.default_avatar), contentDescription = "", - modifier = Modifier - .height(46.dp) - .width(46.dp) - .clip(CircleShape) - .clickable { - Navigate.navigate("profile_screen/" + notification.account?.id, navController) + modifier = Modifier.height(46.dp).width(46.dp).clip(CircleShape).clickable { + navController.navigate(Destination.Profile(notification.account.id)) }) Spacer(modifier = Modifier.width(10.dp)) Column(Modifier.weight(1f)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text(text = notification.account?.username.orEmpty(), fontWeight = FontWeight.Bold, modifier = Modifier.clickable { - Navigate.navigate("profile_screen/" + notification.account?.id, navController) + val annotatedText = buildAnnotatedString { + pushStringAnnotation(tag = "username", annotation = notification.account.id) + withStyle( + style = SpanStyle( + fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground + ) + ) { + append(notification.account.username) + } + pop() + append(" ") + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.onBackground)) { + append(text) + } + } + + ClickableText( + text = annotatedText, + style = MaterialTheme.typography.bodyMedium, + onClick = { offset -> + annotatedText.getStringAnnotations( + tag = "username", start = offset, end = offset + ).firstOrNull()?.let { annotation -> + if (annotation.tag == "username") { + navController.navigate(Destination.Profile(annotation.item)) + } + } ?: kotlin.run { + if (notification.post != null && notification.post.mediaAttachments.isEmpty()) { + navController.navigate(Destination.Mention(notification.post.id)) + } else if (notification.post != null && notification.post.mediaAttachments.isNotEmpty()) { + navController.navigate(Destination.Post(notification.post.id)) + } else if (notification.post == null) { + navController.navigate(Destination.Profile(notification.account.id)) + } + } }) - Text(text = text, overflow = TextOverflow.Ellipsis) - } Text( - text = TimeAgo.convertTimeToText(notification.createdAt), + text = timeAgoText.value, fontSize = 14.sp, color = MaterialTheme.colorScheme.primary ) } - val doesMediaAttachmentExsist = (notification.post?.mediaAttachments?.size - ?: 0) > 0 - if (showImage && (doesMediaAttachmentExsist || (viewModel.ancestor != null && viewModel.ancestor!!.mediaAttachments.isNotEmpty())) - ) { + val doesMediaAttachmentExsist = (notification.post?.mediaAttachments?.size ?: 0) > 0 + if (showImage && (doesMediaAttachmentExsist || (viewModel.ancestor != null && viewModel.ancestor!!.mediaAttachments.isNotEmpty()))) { val previewUrl = if (doesMediaAttachmentExsist) { notification.post?.mediaAttachments?.get(0)?.previewUrl } else { viewModel.ancestor?.mediaAttachments?.get(0)?.previewUrl } - //Spacer(modifier = Modifier.weight(1f)) - AsyncImage(model = previewUrl, + Spacer(modifier = Modifier.width(10.dp)) + AsyncImage( + model = previewUrl, contentDescription = "", contentScale = ContentScale.Crop, - modifier = Modifier - .height(36.dp) - .aspectRatio(1f) - .clip(RoundedCornerShape(4.dp)) + modifier = Modifier.height(36.dp).aspectRatio(1f).clip(RoundedCornerShape(4.dp)) .clickable { - Navigate.navigate( - "single_post_screen/" + if (doesMediaAttachmentExsist) {notification.post!!.id} else {viewModel.ancestor!!.id + "?openReplies=true"}, navController + navController.navigate( + Destination.Post( + id = if (doesMediaAttachmentExsist) { + notification.account.id + } else { + viewModel.ancestor!!.id + }, openReplies = true + ) ) }) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/notifications/NotificationsComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/notifications/NotificationsComposable.kt index 73912c72..91557215 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/notifications/NotificationsComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/notifications/NotificationsComposable.kt @@ -47,8 +47,6 @@ import com.daniebeler.pfpixelix.ui.composables.states.EndOfListComposable import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposable import com.daniebeler.pfpixelix.ui.composables.states.FullscreenEmptyStateComposable import com.daniebeler.pfpixelix.ui.composables.states.LoadingComposable -import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.LocalKmpContext import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res @@ -70,7 +68,6 @@ fun NotificationsComposable( val lazyListState = rememberLazyListState() val scrollState = rememberScrollState() - val context: KmpContext = LocalKmpContext.current Scaffold(contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top), topBar = { CenterAlignedTopAppBar(title = { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/CommentsBottomSheet.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/CommentsBottomSheet.kt index ad4b701b..592704b9 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/CommentsBottomSheet.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/CommentsBottomSheet.kt @@ -65,8 +65,7 @@ import com.daniebeler.pfpixelix.ui.composables.post.reply.ReplyElementViewModel import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposable import com.daniebeler.pfpixelix.ui.composables.states.FixedHeightLoadingComposable import com.daniebeler.pfpixelix.ui.composables.textfield_mentions.TextFieldMentionsComposable -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.TimeAgo import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res @@ -82,7 +81,6 @@ fun CommentsBottomSheet( post: Post, navController: NavController, viewModel: PostViewModel ) { var replyText by remember { mutableStateOf(TextFieldValue("")) } - val context = LocalKmpContext.current Box( modifier = Modifier .fillMaxSize() @@ -122,7 +120,7 @@ fun CommentsBottomSheet( navController = navController, {}, viewModel.myAccountId, - { url -> viewModel.openUrl(url, context) }) + { url -> viewModel.openUrl(url) }) } TextFieldMentionsComposable(submit = { text -> @@ -182,7 +180,7 @@ fun CommentsBottomSheet( navController = navController, { viewModel.deleteReply(reply.id) }, viewModel.myAccountId, - { url -> viewModel.openUrl(url, context) }) + { url -> viewModel.openUrl(url) }) } if (viewModel.repliesState.isLoading) { @@ -262,9 +260,7 @@ private fun ReplyElement( .width(42.dp) .clip(CircleShape) .clickable { - Navigate.navigate( - "profile_screen/" + reply.account.id, navController - ) + navController.navigate(Destination.Profile(reply.account.id)) }) Spacer(modifier = Modifier.width(12.dp)) @@ -275,9 +271,7 @@ private fun ReplyElement( fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.clickable { - Navigate.navigate( - "profile_screen/" + reply.account.id, navController - ) + navController.navigate(Destination.Profile(reply.account.id)) }) Text( diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/LikesBottomSheet.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/LikesBottomSheet.kt index 5f6b5a7d..df28d095 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/LikesBottomSheet.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/LikesBottomSheet.kt @@ -28,10 +28,11 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.domain.model.Account -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res -import pixelix.app.generated.resources.followers +import pixelix.app.generated.resources.follower import pixelix.app.generated.resources.liked_by import pixelix.app.generated.resources.no_likes_yet @@ -92,7 +93,7 @@ private fun LikedByAccountElement(account: Account, navController: NavController .padding(horizontal = 12.dp, vertical = 8.dp) .fillMaxWidth() .clickable { - Navigate.navigate("profile_screen/" + account.id, navController) + navController.navigate(Destination.Profile(account.id)) }, verticalAlignment = Alignment.CenterVertically ) { AsyncImage( @@ -107,7 +108,7 @@ private fun LikedByAccountElement(account: Account, navController: NavController Column { Text(text = "@${account.username}") Text( - text = "${account.followersCount} " + stringResource(Res.string.followers), + text = "${account.followersCount} " + pluralStringResource(Res.plurals.follower, account.followersCount), fontSize = 14.sp, color = MaterialTheme.colorScheme.primary ) 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 e9e9f210..2bbb9827 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 @@ -31,6 +31,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.outlined.Cached +import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Description import androidx.compose.material.icons.outlined.LocationOn @@ -46,11 +47,11 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -74,25 +75,24 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.zIndex import androidx.navigation.NavController -import co.touchlab.kermit.Logger import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter +import com.daniebeler.pfpixelix.di.LocalAppComponent import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.MediaAttachment import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.ui.composables.hashtagMentionText.HashtagsMentionsTextView import com.daniebeler.pfpixelix.ui.composables.states.LoadingComposable +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.BlurHashDecoder -import com.daniebeler.pfpixelix.utils.KmpUri -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.Navigate -import com.daniebeler.pfpixelix.utils.toKmpUri +import com.daniebeler.pfpixelix.utils.TimeAgo import com.daniebeler.pfpixelix.utils.zoomable.rememberZoomState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import net.engawapg.lib.zoomable.snapBackZoomable +import net.engawapg.lib.zoomable.zoomable import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource @@ -115,6 +115,7 @@ import pixelix.app.generated.resources.ok import pixelix.app.generated.resources.others import pixelix.app.generated.resources.reblogged_by import pixelix.app.generated.resources.sync_outline +import pixelix.app.generated.resources.sync_outline_bold import pixelix.app.generated.resources.this_action_cannot_be_undone import pixelix.app.generated.resources.view_comments @@ -132,9 +133,6 @@ fun PostComposable( updatePost: (post: Post) -> Unit = {}, viewModel: PostViewModel = injectViewModel(key = "post" + post.id) { postViewModel } ) { - - val context = LocalKmpContext.current - var postId by remember { mutableStateOf(post.id) } val sheetState = rememberModalBottomSheetState() var showBottomSheet by remember { @@ -147,9 +145,8 @@ fun PostComposable( ) } - DisposableEffect(post.createdAt) { - viewModel.convertTime(post.createdAt) - onDispose {} + val timeAgoText = produceState(initialValue = "") { + value = TimeAgo.convertTimeToText(post.createdAt) } LaunchedEffect(Unit) { @@ -197,7 +194,8 @@ fun PostComposable( var animateHeart by remember { mutableStateOf(false) } - val heartScale by animateFloatAsState(targetValue = if (animateHeart) 1.3f else 1f, + val heartScale by animateFloatAsState( + targetValue = if (animateHeart) 1.3f else 1f, animationSpec = tween(durationMillis = 200, easing = LinearEasing), finishedListener = { animateHeart = false @@ -211,9 +209,7 @@ fun PostComposable( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(start = 16.dp, end = 12.dp).clickable(onClick = { - Navigate.navigate( - "profile_screen/" + reblogAccount.id, navController - ) + navController.navigate(Destination.Profile(reblogAccount.id)) }) ) { Icon(Icons.Outlined.Cached, contentDescription = "reblogged by") @@ -229,9 +225,7 @@ fun PostComposable( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 16.dp, end = 12.dp).clickable(onClick = { - Navigate.navigate( - "profile_screen/" + viewModel.post!!.account.id, navController - ) + navController.navigate(Destination.Profile(viewModel.post!!.account.id)) }) ) { AsyncImage( @@ -287,9 +281,9 @@ fun PostComposable( Spacer(modifier = Modifier.height(6.dp)) if (viewModel.post!!.mediaAttachments.isNotEmpty()) { - if (viewModel.post!!.sensitive && !viewModel.showPost) { + if (viewModel.post!!.sensitive && !viewModel.showPost && viewModel.blurSensitiveContent) { - Box { + Box(modifier.padding(start = 12.dp, end = 12.dp).clip(RoundedCornerShape(16.dp))) { val blurHashBitmap = BlurHashDecoder.decode( viewModel.post!!.mediaAttachments[0].blurHash ) @@ -333,11 +327,13 @@ fun PostComposable( } else { if (viewModel.post!!.mediaAttachments.count() > 1) { - Box( - - ) { + val smallestAspectRatio = viewModel.post!!.mediaAttachments + .minByOrNull { it.meta?.original?.aspect ?: 1.0 } + Box { HorizontalPager( - state = pagerState, modifier = Modifier.zIndex(50f) + state = pagerState, modifier = Modifier.zIndex(50f).aspectRatio( + smallestAspectRatio?.meta?.original?.aspect?.toFloat() ?: 1f + ) ) { page -> Box( modifier = Modifier.zIndex(10f) @@ -406,7 +402,7 @@ fun PostComposable( mentions = viewModel.post!!.mentions, navController = navController, textSize = 18.sp, - openUrl = { url -> viewModel.openUrl(url, context) }, + openUrl = { url -> viewModel.openUrl(url) }, modifier = Modifier.padding(top = 16.dp, bottom = 16.dp) ) HorizontalDivider() @@ -483,7 +479,7 @@ fun PostComposable( viewModel.unreblogPost(postId, updatePost) }) { Icon( - imageVector = vectorResource(Res.drawable.sync_outline), + imageVector = vectorResource(Res.drawable.sync_outline_bold), contentDescription = "undo reblog post", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.rotate(boostRotation) @@ -530,23 +526,22 @@ fun PostComposable( Text( text = stringResource(Res.string.liked_by) + " ", fontSize = 14.sp ) - Text(text = viewModel.post!!.likedBy!!.username!!, + Text( + text = viewModel.post!!.likedBy!!.username!!, fontSize = 14.sp, fontWeight = FontWeight.Bold, modifier = Modifier.clickable { - Navigate.navigate( - "profile_screen/" + viewModel.post!!.likedBy!!.id, - navController - ) + navController.navigate(Destination.Profile(viewModel.post!!.likedBy!!.id!!)) }) if (post.favouritesCount > 1) { Text( text = " " + stringResource(Res.string.and) + " ", fontSize = 14.sp ) - Text(text = (viewModel.post!!.favouritesCount - 1).toString() + " " + stringResource( - Res.string.others - ), + Text( + text = (viewModel.post!!.favouritesCount - 1).toString() + " " + stringResource( + Res.string.others + ), fontWeight = FontWeight.Bold, fontSize = 14.sp, modifier = Modifier.clickable { @@ -569,7 +564,7 @@ fun PostComposable( text = viewModel.post!!.content, mentions = viewModel.post!!.mentions, navController = navController, - openUrl = { url -> viewModel.openUrl(url, context) }, + openUrl = { url -> viewModel.openUrl(url) }, maximumLines = 4 ) } @@ -579,9 +574,10 @@ fun PostComposable( Spacer(modifier = Modifier.height(6.dp)) - Text(text = stringResource( - Res.string.view_comments, viewModel.post!!.replyCount - ), + Text( + text = stringResource( + Res.string.view_comments, viewModel.post!!.replyCount + ), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.clickable { viewModel.loadReplies( @@ -592,7 +588,7 @@ fun PostComposable( } Text( - text = viewModel.timeAgoString, + text = timeAgoText.value, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -609,15 +605,13 @@ fun PostComposable( onDismissRequest = { showBottomSheet = 0 }, - sheetState = sheetState, - modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars) + sheetState = sheetState ) { if (showBottomSheet == 1) { CommentsBottomSheet(post, navController, viewModel) } else if (showBottomSheet == 2) { if (viewModel.myAccountId != null && post.account.id == viewModel.myAccountId) { ShareBottomSheet( - context, post.url, true, viewModel, @@ -627,7 +621,6 @@ fun PostComposable( ) } else { ShareBottomSheet( - context, post.url, false, viewModel, @@ -737,14 +730,13 @@ fun PostImage( }) }) { if (mediaAttachment.type == "image") { - ImageWrapper(mediaAttachment, + ImageWrapper( + mediaAttachment, { zoomState.setContentSize(it.painter.intrinsicSize) }, { imageLoaded = true }) } else { VideoAttachment( - mediaAttachment, - viewModel, - { imageLoaded = true }) + mediaAttachment, viewModel, { imageLoaded = true }) } } @@ -800,7 +792,8 @@ private fun ImageWrapper( setContentSize: (painter: AsyncImagePainter.State.Success) -> Unit, onSuccess: () -> Unit ) { - AsyncImage(model = mediaAttachment.url, + AsyncImage( + model = mediaAttachment.url, contentDescription = "", Modifier.fillMaxWidth(), contentScale = ContentScale.FillWidth, @@ -847,18 +840,24 @@ fun MediaDialog( properties = DialogProperties(usePlatformDefaultWidth = false) ) { Box( - modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.8f)) - .clickable(onClick = closeDialog), // Close on background tap + modifier = Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.8f)).clickable { + closeDialog() + }, contentAlignment = Alignment.Center ) { - Box(modifier = Modifier.zIndex(2f).snapBackZoomable(zoomState)) { + Box(modifier = Modifier.zIndex(2f).zoomable(zoomState).clickable { }) { if (mediaAttachment.type == "image") { - ImageWrapper(mediaAttachment, + ImageWrapper( + mediaAttachment, { zoomState.setContentSize(it.painter.intrinsicSize) }, {}) } else { VideoAttachment(mediaAttachment, postViewModel, {}) - + } + } + Box(Modifier.align(Alignment.TopEnd).padding(20.dp).zIndex(2f)) { + IconButton(onClick = { closeDialog() }) { + Icon(Icons.Outlined.Close, "", tint = Color.White) } } } 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 dd3b71e7..ce250f83 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 @@ -5,19 +5,21 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.daniebeler.pfpixelix.domain.service.utils.Resource +import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.domain.model.LikedBy +import com.daniebeler.pfpixelix.domain.model.NewReport 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.platform.Platform import com.daniebeler.pfpixelix.domain.service.post.PostService import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences 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 com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.TimeAgo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -32,7 +34,8 @@ class PostViewModel @Inject constructor( private val postEditorService: PostEditorService, private val authService: AuthService, private val accountService: AccountService, - private val platform: Platform + private val platform: Platform, + private val fileService: FileService ) : ViewModel() { var post: Post? by mutableStateOf(null) @@ -45,16 +48,16 @@ class PostViewModel @Inject constructor( var deleteState by mutableStateOf(DeleteState()) var deleteDialog: String? by mutableStateOf(null) - var timeAgoString: String by mutableStateOf("") - + var reportState by mutableStateOf(null) var showPost: Boolean by mutableStateOf(false) var myAccountId: String? = null var myUsername: String? = null - var isAltTextButtonHidden by mutableStateOf(false) var isInFocusMode by mutableStateOf(false) + var isAutoplayVideos by mutableStateOf(true) + var blurSensitiveContent by mutableStateOf(false) var volume by mutableStateOf(prefs.enableVolume) @@ -70,6 +73,12 @@ class PostViewModel @Inject constructor( viewModelScope.launch { prefs.focusModeFlow.collect { isInFocusMode = it } } + viewModelScope.launch { + prefs.autoplayVideoFlow.collect { isAutoplayVideos = it } + } + viewModelScope.launch { + prefs.blurSensitiveContentFlow.collect { blurSensitiveContent = it } + } } fun toggleVolume(newVolume: Boolean) { @@ -113,10 +122,6 @@ class PostViewModel @Inject constructor( showPost = !showPost } - fun convertTime(createdAt: String) { - timeAgoString = TimeAgo.convertTimeToText(createdAt) - } - fun loadReplies(postId: String) { postService.getReplies(postId).onEach { result -> repliesState = when (result) { @@ -167,11 +172,11 @@ class PostViewModel @Inject constructor( } is Resource.Error -> { - println(result.message) + Logger.e(result.message) } is Resource.Loading -> { - println("is loading") + Logger.v("is loading") } } }.launchIn(viewModelScope) @@ -242,10 +247,27 @@ class PostViewModel @Inject constructor( return } post = post?.copy( - favourited = false, favouritesCount = post?.favouritesCount?.minus( + favourited = false, + favouritesCount = post?.favouritesCount?.minus( 1 - ) ?: 0 + ) ?: 0, ) + + post?.likedBy?.let { + if (it.username == myUsername) { + post = post!!.copy( + likedBy = post!!.likedBy!!.copy( + username = null, + totalCount = post!!.likedBy!!.totalCount - 1 + ) + ) + } else { + post = post!!.copy( + likedBy = post!!.likedBy!!.copy(totalCount = post!!.likedBy!!.totalCount - 1) + ) + } + } + post?.let { updatePost(it) } CoroutineScope(Dispatchers.Default).launch { @@ -253,7 +275,7 @@ class PostViewModel @Inject constructor( when (result) { is Resource.Success -> { post = post?.copy(favourited = result.data?.favourited ?: false) - result.data?.let { updatePost(result.data) } + post?.let { updatePost(it) } } is Resource.Error -> { @@ -280,7 +302,7 @@ class PostViewModel @Inject constructor( when (result) { is Resource.Success -> { post = post?.copy(reblogged = result.data?.reblogged ?: false) - result.data?.let { updatePost(result.data) } + post?.let { updatePost(it) } } is Resource.Error -> { @@ -305,7 +327,7 @@ class PostViewModel @Inject constructor( when (result) { is Resource.Success -> { post = post?.copy(reblogged = result.data?.reblogged ?: false) - result.data?.let { updatePost(result.data) } + post?.let { updatePost(it) } } is Resource.Error -> { @@ -329,8 +351,8 @@ class PostViewModel @Inject constructor( postService.bookmarkPost(postId).onEach { result -> when (result) { is Resource.Success -> { - post = post?.copy(bookmarked = result.data?.bookmarked ?: false) - result.data?.let { updatePost(result.data) } + post = post?.copy(bookmarked = result.data.bookmarked) + post?.let { updatePost(it) } } is Resource.Error -> { @@ -355,7 +377,7 @@ class PostViewModel @Inject constructor( when (result) { is Resource.Success -> { post = post?.copy(bookmarked = result.data?.bookmarked ?: false) - result.data?.let { updatePost(result.data) } + post?.let { updatePost(it) } } is Resource.Error -> { @@ -369,15 +391,50 @@ class PostViewModel @Inject constructor( }.launchIn(viewModelScope) } } + } + + fun reportPost(category: String) { + reportState = ReportState(isLoading = true, reported = false) + if (post == null) { + reportState = ReportState(isLoading = false, reported = false, error = "an unexpected error occurred") + return + } + val newReport = NewReport( + reportType = category, + objectType = ReportObjectType.POST, + objectId = post!!.id + ) + CoroutineScope(Dispatchers.Default).launch { + postService.reportPost(newReport).onEach { result -> + reportState = when (result) { + is Resource.Success -> { + ReportState( + reported = true + ) + } + is Resource.Error -> { + ReportState( + error = "an unexpected error occured" + ) + } + + is Resource.Loading -> { + ReportState( + isLoading = true + ) + } + } + }.launchIn(viewModelScope) + } } - fun openUrl(url: String, context: KmpContext) { + fun openUrl(url: String) { platform.openUrl(url) } - fun saveImage(name: String?, url: String, context: KmpContext) { - platform.downloadImageToGallery(name, url) + fun saveImage(name: String?, url: String) { + fileService.downloadFile(name, url) } fun shareText(text: String) { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ReportDialog.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ReportDialog.kt new file mode 100644 index 00000000..a1d5c508 --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ReportDialog.kt @@ -0,0 +1,112 @@ +package com.daniebeler.pfpixelix.ui.composables.post + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.resources.vectorResource +import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.abusive_or_harmful +import pixelix.app.generated.resources.adult_or_sensitive_content +import pixelix.app.generated.resources.cancel +import pixelix.app.generated.resources.chevron_forward_outline +import pixelix.app.generated.resources.copyright_infringement +import pixelix.app.generated.resources.impersonation +import pixelix.app.generated.resources.ok +import pixelix.app.generated.resources.scam +import pixelix.app.generated.resources.spam +import pixelix.app.generated.resources.terrorism +import pixelix.app.generated.resources.underage_account +import pixelix.app.generated.resources.violence +import pixelix.app.generated.resources.report +import pixelix.app.generated.resources.reported + +@Composable +fun ReportDialog( + dismissDialog: () -> Unit, + reportState: ReportState?, + report: (category: String) -> Unit +) { + Dialog( + onDismissRequest = dismissDialog, + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + if (reportState != null) { + if (reportState.isLoading) { + CircularProgressIndicator() + } else if (reportState.error.isNotBlank()) { + Text("an unexpected error occurred") + } else { + Text(stringResource(Res.string.reported)) + } + HorizontalDivider() + Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = { dismissDialog() }) { + Text(stringResource(Res.string.ok)) + } + } + } else { + Text(stringResource(Res.string.report), style = MaterialTheme.typography.headlineSmall) + HorizontalDivider() + ReportCategoryButton(Res.string.spam, { report("spam") }) + ReportCategoryButton(Res.string.adult_or_sensitive_content, { report("sensitive") }) + ReportCategoryButton(Res.string.abusive_or_harmful, { report("abusive") }) + ReportCategoryButton(Res.string.underage_account, { report("underage") }) + ReportCategoryButton(Res.string.violence, { report("violence") }) + ReportCategoryButton(Res.string.copyright_infringement, { report("copyright") }) + ReportCategoryButton(Res.string.impersonation, { report("impersonation") }) + ReportCategoryButton(Res.string.scam, { report("scam") }) + ReportCategoryButton(Res.string.terrorism, { report("terrorism") }) + + HorizontalDivider() + Row (modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = { dismissDialog() }) { + Text(stringResource(Res.string.cancel)) + } + } + } + } + } + } +} + +@Composable +private fun ReportCategoryButton(category: StringResource, onClick: () -> Unit) { + Row(Modifier.fillMaxWidth().clickable { onClick() }) { + Text(stringResource(category)) + Icon( + imageVector = vectorResource(Res.drawable.chevron_forward_outline), + contentDescription = stringResource(category), + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 4.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ReportState.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ReportState.kt new file mode 100644 index 00000000..09e9384a --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/ReportState.kt @@ -0,0 +1,7 @@ +package com.daniebeler.pfpixelix.ui.composables.post + +data class ReportState( + val isLoading: Boolean = false, + val reported: Boolean = false, + val error: 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 085a9294..20aacb7f 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 @@ -25,8 +25,7 @@ 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.utils.KmpContext -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource @@ -48,10 +47,11 @@ 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( - context: KmpContext, url: String, minePost: Boolean, viewModel: PostViewModel, @@ -64,6 +64,8 @@ fun ShareBottomSheet( mutableStateOf("") } + var isReportDialogOpen by remember { mutableStateOf(false) } + val mediaAttachment: MediaAttachment? = viewModel.post?.mediaAttachments?.let { attachments -> if (attachments.isNotEmpty() && currentMediaAttachmentNumber in attachments.indices) { attachments[currentMediaAttachmentNumber] @@ -99,36 +101,39 @@ fun ShareBottomSheet( Text(text = stringResource(Res.string.visibility_x, humanReadableVisibility)) } if (mediaAttachment?.license != null) { - ButtonRowElement(icon = Res.drawable.document_text_outline, text = stringResource( - Res.string.license, mediaAttachment.license.title - ), onClick = { - viewModel.openUrl(mediaAttachment.license.url, context) - }) + ButtonRowElement( + icon = Res.drawable.document_text_outline, text = stringResource( + Res.string.license, mediaAttachment.license.title + ), onClick = { + viewModel.openUrl(mediaAttachment.license.url) + }) } HorizontalDivider(Modifier.padding(12.dp)) - ButtonRowElement(icon = Res.drawable.open_outline, text = stringResource( - Res.string.open_in_browser - ), onClick = { - viewModel.openUrl(url, context) - }) + ButtonRowElement( + icon = Res.drawable.open_outline, text = stringResource( + Res.string.open_in_browser + ), onClick = { + viewModel.openUrl(url) + }) - ButtonRowElement(icon = Res.drawable.share_social_outline, + ButtonRowElement( + icon = Res.drawable.share_social_outline, text = stringResource(Res.string.share_this_post), onClick = { viewModel.shareText(url) }) if (mediaAttachment != null && PlatformFeatures.downloadToGallery && mediaAttachment.type == "image") { - ButtonRowElement(icon = Res.drawable.cloud_download_outline, + ButtonRowElement( + icon = Res.drawable.cloud_download_outline, text = stringResource(Res.string.download_image), onClick = { viewModel.saveImage( post.account.username, - viewModel.post!!.mediaAttachments[currentMediaAttachmentNumber].url!!, - context + viewModel.post!!.mediaAttachments[currentMediaAttachmentNumber].url!! ) }) } @@ -140,7 +145,7 @@ fun ShareBottomSheet( icon = Res.drawable.pencil_outline, text = stringResource(Res.string.edit_post), onClick = { - Navigate.navigate("edit_post_screen/${post.id}", navController = navController) + navController.navigate(Destination.EditPost(post.id)) } ) ButtonRowElement( @@ -151,6 +156,30 @@ fun ShareBottomSheet( }, color = MaterialTheme.colorScheme.error ) + } else { + HorizontalDivider(Modifier.padding(12.dp)) + + ButtonRowElement( + icon = Res.drawable.warning, + text = stringResource(Res.string.report_this_post), + onClick = { + isReportDialogOpen = true + }, + color = MaterialTheme.colorScheme.error + ) + } + } + + if (isReportDialogOpen) { + ReportDialog( + dismissDialog = { + isReportDialogOpen = false + viewModel.reportState = null + }, + reportState = viewModel.reportState + ) { category -> + viewModel.reportPost(category) + viewModel.reportState = null } } } 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 cb006c6b..7b6bbf7b 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 @@ -1,8 +1,6 @@ package com.daniebeler.pfpixelix.ui.composables.post -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio @@ -32,8 +30,8 @@ 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.LocalKmpContext import com.daniebeler.pfpixelix.utils.VideoPlayer @Composable @@ -42,16 +40,23 @@ fun VideoAttachment( viewModel: PostViewModel, onReady: () -> Unit ) { - val context = LocalKmpContext.current 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) } var videoFrameIsVisible by remember { mutableStateOf(false) } Column { - Box { + Box(Modifier.clickable { + if (isPlaying) { + player.pause() + } else { + player.play() + } + }) { player.view( modifier = Modifier .fillMaxWidth() @@ -109,6 +114,7 @@ fun VideoAttachment( progress = current.toFloat() / duration.toFloat() } player.hasAudio = { hasAudio = it } + player.isVideoPlaying = { isPlaying = it } onDispose { player.progress = null @@ -118,7 +124,7 @@ fun VideoAttachment( } LaunchedEffect(videoFrameIsVisible) { - if (videoFrameIsVisible) { + if (videoFrameIsVisible && viewModel.isAutoplayVideos) { player.play() } else { player.pause() @@ -130,10 +136,11 @@ fun VideoAttachment( val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_RESUME -> { - if (videoFrameIsVisible) { + if (videoFrameIsVisible && viewModel.isAutoplayVideos) { player.play() } } + Lifecycle.Event.ON_PAUSE -> { player.pause() } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/reply/ReplyElementViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/reply/ReplyElementViewModel.kt index 6572c17c..3921c9c6 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/reply/ReplyElementViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/post/reply/ReplyElementViewModel.kt @@ -5,10 +5,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.daniebeler.pfpixelix.domain.service.utils.Resource +import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.domain.service.editor.PostEditorService import com.daniebeler.pfpixelix.domain.service.post.PostService +import com.daniebeler.pfpixelix.domain.service.utils.Resource import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject @@ -74,10 +75,10 @@ class ReplyElementViewModel @Inject constructor( } is Resource.Error -> { - println(result.message) + Logger.e(result.message) } is Resource.Loading -> { - println("is loading") + Logger.v("is loading") } } }.launchIn(viewModelScope) @@ -95,7 +96,7 @@ class ReplyElementViewModel @Inject constructor( } is Resource.Loading -> { - println("is loading") + Logger.v("is loading") } } }.launchIn(viewModelScope) @@ -113,7 +114,7 @@ class ReplyElementViewModel @Inject constructor( } is Resource.Loading -> { - println("is loading") + Logger.v("is loading") } } }.launchIn(viewModelScope) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/CollectionsComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/CollectionsComposable.kt index ca511495..615c5109 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/CollectionsComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/CollectionsComposable.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.ui.composables.InfiniteListHandler -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res @@ -74,7 +74,7 @@ fun CollectionsComposable( Modifier .padding(12.dp) .clickable { - Navigate.navigate("collection_screen/" + it.id, navController) + navController.navigate(Destination.Collection(it.id)) }, horizontalAlignment = Alignment.CenterHorizontally ) { AsyncImage( diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/MutualFollowersComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/MutualFollowersComposable.kt index 6ad2819a..8f3bbaad 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/MutualFollowersComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/MutualFollowersComposable.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.ui.composables.followers.FollowerElementComposable -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.and @@ -203,8 +203,7 @@ fun MutualFollowersComposable( annotatedString.getStringAnnotations("account", it, it) .firstOrNull()?.let { annotation -> - println(annotation.item) - Navigate.navigate("profile_screen/" + annotation.item, navController, false) + navController.navigate(Destination.Profile(annotation.item)) } }) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/ProfileTopSection.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/ProfileTopSection.kt index b0a086cd..38f9ce20 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/ProfileTopSection.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/ProfileTopSection.kt @@ -33,20 +33,22 @@ import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.domain.model.Account import com.daniebeler.pfpixelix.domain.model.Relationship import com.daniebeler.pfpixelix.ui.composables.hashtagMentionText.HashtagsMentionsTextView -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.StringFormat import kotlinx.datetime.LocalDate import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.char import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.admin import pixelix.app.generated.resources.blocked import pixelix.app.generated.resources.default_avatar -import pixelix.app.generated.resources.followers +import pixelix.app.generated.resources.follower import pixelix.app.generated.resources.following import pixelix.app.generated.resources.follows_you +import pixelix.app.generated.resources.joined_date import pixelix.app.generated.resources.muted import pixelix.app.generated.resources.posts @@ -80,35 +82,31 @@ fun ProfileTopSection( fontWeight = FontWeight.Bold, fontSize = 18.sp ) - Text(text = stringResource(Res.string.posts), fontSize = 12.sp) + Text(text = pluralStringResource(Res.plurals.posts, account.postsCount), fontSize = 12.sp) } Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.clickable { - Navigate.navigate( - "followers_screen/" + "followers/" + account.id, navController - ) + navController.navigate(Destination.Followers(account.id, true)) }) { Text( text = StringFormat.groupDigits(account.followersCount), fontWeight = FontWeight.Bold, fontSize = 18.sp ) - Text(text = stringResource(Res.string.followers), fontSize = 12.sp) + Text(text = pluralStringResource(Res.plurals.follower, account.followersCount), fontSize = 12.sp) } Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.clickable { - Navigate.navigate( - "followers_screen/" + "following/" + account.id, navController - ) + navController.navigate(Destination.Followers(account.id, false)) }) { Text( text = StringFormat.groupDigits(account.followingCount), fontWeight = FontWeight.Bold, fontSize = 18.sp ) - Text(text = stringResource(Res.string.following), fontSize = 12.sp) + Text(text = pluralStringResource(Res.plurals.following, account.followingCount), fontSize = 12.sp) } } } @@ -194,7 +192,10 @@ fun ProfileTopSection( year() } Text( - text = "Joined ${formatter.format(date)}", + text = stringResource( + Res.string.joined_date, + formatter.format(date) + ), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 10.sp ) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileComposable.kt index c9d794fe..5f495f86 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileComposable.kt @@ -67,8 +67,7 @@ import com.daniebeler.pfpixelix.ui.composables.profile.ProfileTopSection import com.daniebeler.pfpixelix.ui.composables.profile.SwitchViewComposable import com.daniebeler.pfpixelix.ui.composables.profile.server_stats.DomainSoftwareComposable import com.daniebeler.pfpixelix.ui.composables.states.EmptyState -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource @@ -122,7 +121,6 @@ fun OtherProfileComposable( byUsername: String?, viewModel: OtherProfileViewModel = injectViewModel(key = "other-profile$userId$byUsername") { otherProfileViewModel } ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val lazyGridState = rememberLazyListState() @@ -133,13 +131,11 @@ fun OtherProfileComposable( var showBlockAlert by remember { mutableStateOf(false) } var showUnBlockAlert by remember { mutableStateOf(false) } - val context = LocalKmpContext.current - - LaunchedEffect(Unit) { + LaunchedEffect(userId) { if (userId != "") { - viewModel.loadData(userId, false) + viewModel.loadData(userId, false, navController) } else { - viewModel.loadDataByUsername(byUsername!!, false) + viewModel.loadDataByUsername(byUsername!!, false, navController) } } @@ -186,7 +182,7 @@ fun OtherProfileComposable( }) { paddingValues -> PullToRefreshBox ( isRefreshing = viewModel.accountState.refreshing || viewModel.postsState.refreshing, - onRefresh = { viewModel.loadData(userId, true) }, + onRefresh = { viewModel.loadData(userId, true, navController) }, modifier = Modifier .fillMaxSize() .padding(paddingValues) @@ -203,7 +199,7 @@ fun OtherProfileComposable( relationship = viewModel.relationshipState.accountRelationship, navController, openUrl = { url -> - viewModel.openUrl(url, context) + viewModel.openUrl(url) }) } @@ -268,9 +264,7 @@ fun OtherProfileComposable( Button( onClick = { viewModel.accountState.account?.let { account -> - Navigate.navigate( - "chat/" + account.id, navController - ) + navController.navigate(Destination.Chat(account.id)) } }, modifier = Modifier.weight(1f), @@ -290,7 +284,7 @@ fun OtherProfileComposable( getMoreCollections = {viewModel.getCollections(account.id, true)}, navController = navController, instanceDomain = viewModel.domain, - openUrl = { url -> viewModel.openUrl(url, context) }) + openUrl = { url -> viewModel.openUrl(url) }) } HorizontalDivider(Modifier.padding(bottom = 12.dp, top = 12.dp)) @@ -335,7 +329,7 @@ fun OtherProfileComposable( } } - ToTopButton(listState = lazyGridState, refresh = {viewModel.loadData(userId, true)}) + ToTopButton(listState = lazyGridState, refresh = {viewModel.loadData(userId, true, navController)}) InfiniteListHandler(lazyListState = lazyGridState) { viewModel.getPostsPaginated(viewModel.userId) @@ -389,7 +383,7 @@ fun OtherProfileComposable( ButtonRowElement(icon = Res.drawable.browsers_outline, text = stringResource( Res.string.open_in_browser ), onClick = { - viewModel.openUrl(viewModel.accountState.account!!.url, context) + viewModel.openUrl(viewModel.accountState.account!!.url) }) ButtonRowElement(icon = Res.drawable.share_social_outline, diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileViewModel.kt index aba83096..8183e12d 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/other_profile/OtherProfileViewModel.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.navigation.NavController import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.domain.repository.PixelfedApi @@ -15,6 +16,7 @@ import com.daniebeler.pfpixelix.domain.service.hashtag.SearchService import com.daniebeler.pfpixelix.domain.service.platform.Platform import com.daniebeler.pfpixelix.domain.service.post.PostService import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences +import com.daniebeler.pfpixelix.domain.service.session.AuthService import com.daniebeler.pfpixelix.domain.service.utils.Resource import com.daniebeler.pfpixelix.ui.composables.profile.AccountState import com.daniebeler.pfpixelix.ui.composables.profile.CollectionsState @@ -22,7 +24,7 @@ import com.daniebeler.pfpixelix.ui.composables.profile.MutualFollowersState import com.daniebeler.pfpixelix.ui.composables.profile.PostsState import com.daniebeler.pfpixelix.ui.composables.profile.RelationshipState import com.daniebeler.pfpixelix.ui.composables.profile.ViewEnum -import com.daniebeler.pfpixelix.utils.KmpContext +import com.daniebeler.pfpixelix.ui.navigation.Destination import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -36,6 +38,7 @@ class OtherProfileViewModel( private val platform: Platform, private val prefs: UserPreferences, private val collectionService: CollectionService, + private val authService: AuthService ) : ViewModel() { var userId: String = "" var accountState by mutableStateOf(AccountState()) @@ -48,10 +51,18 @@ class OtherProfileViewModel( var domain by mutableStateOf("") var view by mutableStateOf(ViewEnum.Grid) - fun loadData(_userId: String, refreshing: Boolean) { + fun loadData(_userId: String, refreshing: Boolean, navController: NavController) { + val myAccountId = authService.getCurrentSession()!!.accountId + + if (_userId == myAccountId) { + navController.popBackStack() + navController.navigate(Destination.OwnProfile) + } + userId = _userId getAccount(userId, refreshing) loadDataExceptAccount(refreshing) + } private fun loadDataExceptAccount(refreshing: Boolean) { @@ -69,7 +80,12 @@ class OtherProfileViewModel( } } - fun loadDataByUsername(username: String, refreshing: Boolean) { + fun loadDataByUsername(username: String, refreshing: Boolean, navController: NavController) { + val myUsername = authService.getCurrentSession()!!.username + if (username == myUsername) { + navController.popBackStack() + navController.navigate(Destination.OwnProfile) + } getAccountByUsername(username, refreshing) } @@ -132,7 +148,11 @@ class OtherProfileViewModel( } is Resource.Loading -> { - AccountState(isLoading = true, account = accountState.account, refreshing = refreshing) + AccountState( + isLoading = true, + account = accountState.account, + refreshing = refreshing + ) } } @@ -157,7 +177,11 @@ class OtherProfileViewModel( } is Resource.Loading -> { - AccountState(isLoading = true, account = accountState.account, refreshing = refreshing) + AccountState( + isLoading = true, + account = accountState.account, + refreshing = refreshing + ) } } @@ -184,12 +208,16 @@ class OtherProfileViewModel( CollectionsState(collections = result.data ?: emptyList()) } else { val endReached = result.data!!.isEmpty() - CollectionsState(collections = collectionsState.collections + result.data, endReached = endReached) + CollectionsState( + collections = collectionsState.collections + result.data, + endReached = endReached + ) } } is Resource.Error -> { - collectionsState = CollectionsState(error = result.message ?: "An unexpected error occurred") + collectionsState = + CollectionsState(error = result.message ?: "An unexpected error occurred") } is Resource.Loading -> { @@ -202,6 +230,9 @@ class OtherProfileViewModel( } private fun getPostsFirstLoad(userId: String, refreshing: Boolean) { + if (postsState.posts.isNotEmpty() && !refreshing) { + return + } postService.getPostsOfAccount(userId).onEach { result -> postsState = when (result) { is Resource.Success -> { @@ -358,7 +389,7 @@ class OtherProfileViewModel( }.launchIn(viewModelScope) } - fun openUrl(url: String, context: KmpContext) { + fun openUrl(url: String) { platform.openUrl(url) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/AccountSwitchBottomSheet.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/AccountSwitchBottomSheet.kt index 102734ff..dd38eafc 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/AccountSwitchBottomSheet.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/AccountSwitchBottomSheet.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -29,10 +30,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import com.daniebeler.pfpixelix.utils.Destinations +import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.model.credentialsToAccount import com.daniebeler.pfpixelix.ui.composables.custom_account.CustomAccount +import com.daniebeler.pfpixelix.ui.navigation.Destination import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -54,7 +56,9 @@ fun AccountSwitchBottomSheet( viewModel: AccountSwitchViewModel = injectViewModel(key = "account_switcher_viewmodel") { accountSwitchViewModel } ) { val showRemoveLoginDataAlert = remember { mutableStateOf("") } - + LaunchedEffect(Unit) { + viewModel.loadData() + } Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { val sessionStorage = viewModel.sessionStorage if (viewModel.currentCredentials != null) { @@ -98,7 +102,7 @@ fun AccountSwitchBottomSheet( .padding(horizontal = 12.dp, vertical = 8.dp) .fillMaxWidth() .clickable { - navController.navigate(Destinations.NewLogin.route) + navController.navigate(Destination.NewLogin) closeBottomSheet() }, verticalAlignment = Alignment.CenterVertically diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/AccountSwitchViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/AccountSwitchViewModel.kt index 0d790122..5ec2e8e9 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/AccountSwitchViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/AccountSwitchViewModel.kt @@ -8,7 +8,6 @@ import androidx.lifecycle.viewModelScope import com.daniebeler.pfpixelix.domain.service.session.AuthService import com.daniebeler.pfpixelix.domain.service.session.Credentials import com.daniebeler.pfpixelix.domain.service.session.SessionStorage -import com.daniebeler.pfpixelix.utils.Navigate import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject @@ -18,7 +17,8 @@ class AccountSwitchViewModel @Inject constructor( ) : ViewModel() { var sessionStorage by mutableStateOf(null) var currentCredentials by mutableStateOf(null) - init { + + fun loadData() { viewModelScope.launch { loadCurrentCredentials() } @@ -45,7 +45,6 @@ class AccountSwitchViewModel @Inject constructor( } coroutine.invokeOnCompletion { - Navigate.changeAccount() changedAccount() loadAccounts() } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/ModalBottomSheetContent.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/ModalBottomSheetContent.kt index a53588a4..f9847a63 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/ModalBottomSheetContent.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/ModalBottomSheetContent.kt @@ -8,12 +8,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.daniebeler.pfpixelix.ui.composables.ButtonRowElement import com.daniebeler.pfpixelix.ui.composables.ButtonRowElementWithRoundedImage -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.imageResource import org.jetbrains.compose.resources.stringResource @@ -29,7 +28,6 @@ import pixelix.app.generated.resources.heart_outline import pixelix.app.generated.resources.liked_posts import pixelix.app.generated.resources.muted_accounts import pixelix.app.generated.resources.pixelfed_logo -import pixelix.app.generated.resources.pixelix_logo import pixelix.app.generated.resources.remove_circle_outline import pixelix.app.generated.resources.settings import pixelix.app.generated.resources.settings_outline @@ -64,7 +62,7 @@ fun ModalBottomSheetContent( text = stringResource(Res.string.liked_posts), onClick = { closeBottomSheet() - Navigate.navigate("liked_posts_screen", navController) + navController.navigate(Destination.LikedPosts) }) ButtonRowElement( @@ -72,7 +70,7 @@ fun ModalBottomSheetContent( text = stringResource(Res.string.bookmarked_posts), onClick = { closeBottomSheet() - Navigate.navigate("bookmarked_posts_screen", navController) + navController.navigate(Destination.BookmarkedPosts) }) ButtonRowElement( @@ -80,7 +78,7 @@ fun ModalBottomSheetContent( text = stringResource(Res.string.followed_hashtags), onClick = { closeBottomSheet() - Navigate.navigate("followed_hashtags_screen", navController) + navController.navigate(Destination.FollowedHashtags) }) ButtonRowElement( @@ -88,7 +86,7 @@ fun ModalBottomSheetContent( text = stringResource(Res.string.muted_accounts), onClick = { closeBottomSheet() - Navigate.navigate("muted_accounts_screen", navController) + navController.navigate(Destination.MutedAccounts) }) ButtonRowElement( @@ -96,7 +94,7 @@ fun ModalBottomSheetContent( text = stringResource(Res.string.blocked_accounts), onClick = { closeBottomSheet() - Navigate.navigate("blocked_accounts_screen", navController) + navController.navigate(Destination.BlockedAccounts) }) HorizontalDivider(Modifier.padding(12.dp)) @@ -106,7 +104,7 @@ fun ModalBottomSheetContent( text = stringResource(Res.string.about_x, instanceDomain), onClick = { closeBottomSheet() - Navigate.navigate("about_instance_screen", navController) + navController.navigate(Destination.AboutInstance) }) ButtonRowElement( @@ -114,7 +112,7 @@ fun ModalBottomSheetContent( text = stringResource(Res.string.about_pixelix), onClick = { closeBottomSheet() - Navigate.navigate("about_pixelix_screen", navController) + navController.navigate(Destination.AboutPixelix) }) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/OwnProfileComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/OwnProfileComposable.kt index ad67af7d..3fa0b30b 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/OwnProfileComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/OwnProfileComposable.kt @@ -43,7 +43,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import com.daniebeler.pfpixelix.di.LocalAppComponent import com.daniebeler.pfpixelix.di.injectViewModel +import com.daniebeler.pfpixelix.domain.service.platform.PlatformFeatures import com.daniebeler.pfpixelix.ui.composables.InfiniteListHandler import com.daniebeler.pfpixelix.ui.composables.profile.CollectionsComposable import com.daniebeler.pfpixelix.ui.composables.profile.PostsWrapperComposable @@ -52,8 +54,7 @@ import com.daniebeler.pfpixelix.ui.composables.profile.SwitchViewComposable import com.daniebeler.pfpixelix.ui.composables.profile.server_stats.DomainSoftwareComposable import com.daniebeler.pfpixelix.ui.composables.states.EmptyState import com.daniebeler.pfpixelix.ui.composables.states.FullscreenErrorComposable -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.edit_profile @@ -65,7 +66,6 @@ fun OwnProfileComposable( openPreferencesDrawer: () -> Unit, viewModel: OwnProfileViewModel = injectViewModel(key = "own-profile-key") { ownProfileViewModel } ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var showBottomSheet by remember { mutableStateOf(0) } @@ -130,9 +130,7 @@ fun OwnProfileComposable( ) { Button( onClick = { - Navigate.navigate( - "edit_profile_screen", navController - ) + navController.navigate(Destination.EditProfile) }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), @@ -158,7 +156,7 @@ fun OwnProfileComposable( } }, navController = navController, - addNewButton = true, + addNewButton = PlatformFeatures.addCollection, instanceDomain = viewModel.ownDomain, ) { url -> viewModel.openUrl(url) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/OwnProfileViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/OwnProfileViewModel.kt index 7260f849..1a682c5b 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/OwnProfileViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/own_profile/OwnProfileViewModel.kt @@ -98,6 +98,9 @@ class OwnProfileViewModel @Inject constructor( } private fun getPostsFirstLoad(refreshing: Boolean) { + if (postsState.posts.isNotEmpty() && !refreshing) { + return + } postService.getOwnPosts().onEach { result -> postsState = when (result) { is Resource.Success -> { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/server_stats/ServerStatsComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/server_stats/ServerStatsComposable.kt index ec4b77b4..03c344b5 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/server_stats/ServerStatsComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/server_stats/ServerStatsComposable.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.daniebeler.pfpixelix.di.injectViewModel -import com.daniebeler.pfpixelix.utils.LocalKmpContext import com.daniebeler.pfpixelix.utils.StringFormat import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -56,9 +55,6 @@ import pixelix.app.generated.resources.visit_url fun DomainSoftwareComposable( domain: String, viewModel: ServerStatsViewModel = injectViewModel(key = "serverstats$domain") { serverStatsViewModel } ) { - - val context = LocalKmpContext.current - var showBottomSheet by remember { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -176,7 +172,7 @@ fun DomainSoftwareComposable( TextButton( onClick = { viewModel.openUrl( - viewModel.statsState.fediSoftware!!.website, context + viewModel.statsState.fediSoftware!!.website ) }, shape = RoundedCornerShape(12.dp), @@ -319,7 +315,7 @@ fun DomainSoftwareComposable( TextButton( onClick = { viewModel.openUrl( - "https://" + viewModel.statsState.fediServer!!.domain, context + "https://" + viewModel.statsState.fediServer!!.domain ) }, shape = RoundedCornerShape(12.dp), diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/server_stats/ServerStatsViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/server_stats/ServerStatsViewModel.kt index deab9b07..0a1edf18 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/server_stats/ServerStatsViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/profile/server_stats/ServerStatsViewModel.kt @@ -5,11 +5,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.daniebeler.pfpixelix.domain.service.utils.Resource import com.daniebeler.pfpixelix.domain.service.instance.InstanceService import com.daniebeler.pfpixelix.domain.service.platform.Platform +import com.daniebeler.pfpixelix.domain.service.utils.Resource import com.daniebeler.pfpixelix.ui.composables.profile.DomainSoftwareState -import com.daniebeler.pfpixelix.utils.KmpContext import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject @@ -84,7 +83,7 @@ class ServerStatsViewModel @Inject constructor( }.launchIn(viewModelScope) } - fun openUrl(url: String, context: KmpContext) { + fun openUrl(url: String) { platform.openUrl(url) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginComposable.kt index 10cbb2f8..412bf3ca 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginComposable.kt @@ -1,7 +1,9 @@ package com.daniebeler.pfpixelix.ui.composables.session +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -22,6 +24,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -32,7 +36,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowForwardIos import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -42,15 +49,23 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp @@ -69,12 +84,20 @@ import pixelix.app.generated.resources.pixelix_logo_black_xxl import pixelix.app.generated.resources.pixelix_logo_white_xxl import pixelix.app.generated.resources.server_url +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginComposable( isCloseable: Boolean = false, navController: NavController, viewModel: LoginViewModel = injectViewModel("LoginViewModel") { loginViewModel } ) { + val dark = MaterialTheme.colorScheme.background.luminance() < 0.5 + + var expanded by remember { mutableStateOf(false) } + + val filteredServers = viewModel.openServers + .filter { it.domain.startsWith(viewModel.serverHost.text, ignoreCase = true) } + Scaffold(Modifier.fillMaxSize()) { paddingValues -> Column( modifier = Modifier.fillMaxWidth() @@ -85,7 +108,7 @@ fun LoginComposable( modifier = Modifier .fillMaxWidth() .background( - if (isSystemInDarkTheme()) Color.White else Color.Black + if (dark) Color.White else Color.Black ) .windowInsetsPadding( WindowInsets.systemBars.only( @@ -105,7 +128,7 @@ fun LoginComposable( }) { Icon( imageVector = vectorResource(Res.drawable.close_outline), - tint = if (isSystemInDarkTheme()) Color.Black else Color.White, + tint = if (dark) Color.Black else Color.White, contentDescription = "" ) } @@ -116,7 +139,7 @@ fun LoginComposable( .size(150.dp) .clip(CircleShape), painter = painterResource( - if (isSystemInDarkTheme()) { + if (dark) { Res.drawable.pixelix_logo_black_xxl } else { Res.drawable.pixelix_logo_white_xxl @@ -131,7 +154,7 @@ fun LoginComposable( text = "PIXELIX", fontSize = 38.sp, fontWeight = FontWeight.Black, - color = if (isSystemInDarkTheme()) Color.Black else Color.White + color = if (dark) Color.Black else Color.White ) Spacer(modifier = Modifier.height(24.dp)) @@ -139,7 +162,7 @@ fun LoginComposable( Image( painterResource( - if (isSystemInDarkTheme()) { + if (dark) { Res.drawable.login_wave_light } else { Res.drawable.login_wave_dark @@ -177,12 +200,16 @@ fun LoginComposable( keyboardController?.hide() focusManager.clearFocus() viewModel.auth() + expanded = false } Row(verticalAlignment = Alignment.Bottom) { TextField( value = viewModel.serverHost, - onValueChange = { viewModel.updateServerHost(it) }, + onValueChange = { + viewModel.updateServerHost(it) + expanded = true + }, prefix = { Text("https://") }, singleLine = true, modifier = Modifier.weight(1f), @@ -197,6 +224,7 @@ fun LoginComposable( keyboardActions = KeyboardActions(onDone = { login() }) ) + Spacer(Modifier.width(12.dp)) if (viewModel.isLoading) { Box( @@ -216,7 +244,10 @@ fun LoginComposable( } } else { Button( - onClick = { login() }, + onClick = { + login() + expanded = false + }, Modifier .height(56.dp) .width(56.dp) @@ -240,6 +271,43 @@ fun LoginComposable( } } } + + Spacer(Modifier.height(24.dp)) + + AnimatedVisibility( + visible = expanded, + modifier = Modifier.fillMaxWidth() + ) { + Card( + shape = RoundedCornerShape(10.dp), + ) { + + LazyColumn( + modifier = Modifier.padding(horizontal = 12.dp).heightIn(max = 200.dp), + ) { + items( + filteredServers.take(5) + ) { + Row(Modifier.padding(12.dp).fillParentMaxWidth().clickable { + viewModel.updateServerHost( + TextFieldValue( + it.domain, + selection = TextRange(it.domain.length) + ) + ) + expanded = false + }) { + Text(it.domain, fontSize = 20.sp) + } + } + + } + + } + + } + + Spacer(modifier = Modifier.height(24.dp)) TextButton(onClick = { 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 73047f97..a0c15234 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 @@ -3,20 +3,30 @@ package com.daniebeler.pfpixelix.ui.composables.session import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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( private val authService: AuthService, + private val instanceService: InstanceService, private val platform: Platform ) : ViewModel() { - var serverHost by mutableStateOf("") + var serverHost by mutableStateOf(TextFieldValue()) private set var isLoading by mutableStateOf(false) @@ -28,9 +38,32 @@ class LoginViewModel( var error by mutableStateOf(null) private set - fun updateServerHost(host: String) { + var openServers by mutableStateOf>(emptyList()) + private set + + init { + getOpenServers() + } + + private fun getOpenServers() { + instanceService.getOpenServers().onEach { result -> + when (result) { + is Resource.Success -> { + openServers = result.data + } + + is Resource.Error -> { + } + + is Resource.Loading -> { + } + } + }.launchIn(viewModelScope) + } + + fun updateServerHost(host: TextFieldValue) { serverHost = host - isValidHost = authService.isValidHost(serverHost) + isValidHost = authService.isValidHost(serverHost.text) } fun auth() { @@ -38,7 +71,7 @@ class LoginViewModel( try { isLoading = true error = null - authService.auth(serverHost) + authService.auth(serverHost.text) } catch (e: Exception) { error = e.message } finally { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_instance/AboutInstanceComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_instance/AboutInstanceComposable.kt index d4cdd354..04720463 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_instance/AboutInstanceComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_instance/AboutInstanceComposable.kt @@ -36,8 +36,7 @@ import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.ui.composables.states.FullscreenErrorComposable import com.daniebeler.pfpixelix.ui.composables.states.FullscreenLoadingComposable -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import com.daniebeler.pfpixelix.utils.StringFormat import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -62,9 +61,6 @@ fun AboutInstanceComposable( ) { val lazyListState = rememberLazyListState() - - val context = LocalKmpContext.current - Scaffold(contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top), topBar = { CenterAlignedTopAppBar(title = { Text(text = viewModel.ownInstanceDomain, fontWeight = FontWeight.Bold) @@ -137,9 +133,7 @@ fun AboutInstanceComposable( Row(modifier = Modifier .clickable { - Navigate.navigate( - "profile_screen/" + account.id, navController - ) + navController.navigate(Destination.Profile(account.id)) } .padding(horizontal = 12.dp, vertical = 8.dp) .fillMaxWidth(), @@ -179,7 +173,7 @@ fun AboutInstanceComposable( .clickable { if (viewModel.instanceState.instance != null) { viewModel.openUrl( - url = "https://" + viewModel.instanceState.instance!!.domain + "/site/privacy", context + url = "https://" + viewModel.instanceState.instance!!.domain + "/site/privacy" ) } }) @@ -202,7 +196,7 @@ fun AboutInstanceComposable( .clickable { if (viewModel.instanceState.instance != null) { viewModel.openUrl( - url = "https://" + viewModel.instanceState.instance!!.domain + "/site/terms", context + url = "https://" + viewModel.instanceState.instance!!.domain + "/site/terms" ) } }) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_instance/AboutInstanceViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_instance/AboutInstanceViewModel.kt index 500c459e..39412488 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_instance/AboutInstanceViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_instance/AboutInstanceViewModel.kt @@ -5,11 +5,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.daniebeler.pfpixelix.domain.service.utils.Resource 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.utils.KmpContext +import com.daniebeler.pfpixelix.domain.service.utils.Resource import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import me.tatarka.inject.annotations.Inject @@ -47,7 +46,7 @@ class AboutInstanceViewModel @Inject constructor( }.launchIn(viewModelScope) } - fun openUrl(url: String, context: KmpContext) { + fun openUrl(url: String) { platform.openUrl(url) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_pixelix/AboutPixelixComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_pixelix/AboutPixelixComposable.kt index 03594ce6..996852eb 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_pixelix/AboutPixelixComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/about_pixelix/AboutPixelixComposable.kt @@ -30,7 +30,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -43,8 +42,7 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.ui.composables.ButtonRowElement -import com.daniebeler.pfpixelix.utils.LocalKmpContext -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource @@ -56,7 +54,6 @@ import pixelix.app.generated.resources.code_slash_outline import pixelix.app.generated.resources.developed_by import pixelix.app.generated.resources.mastodon_logo import pixelix.app.generated.resources.pixelfed_logo -import pixelix.app.generated.resources.pixelix_logo import pixelix.app.generated.resources.shield_outline import pixelix.app.generated.resources.star_outline @@ -175,9 +172,7 @@ fun AboutPixelixComposable( .width(32.dp) .height(32.dp) .clickable { - Navigate.navigate( - "profile_screen/677938259497057424", navController - ) + navController.navigate(Destination.ProfileByUsername("hiebeler05@pixelix.social")) }) Spacer(modifier = Modifier.width(16.dp)) @@ -224,9 +219,7 @@ fun AboutPixelixComposable( .width(32.dp) .height(32.dp) .clickable { - Navigate.navigate( - "profile_screen/497910174831013185", navController - ) + navController.navigate(Destination.ProfileByUsername("daniebeler@pixelix.social")) }) Spacer(modifier = Modifier.width(16.dp)) @@ -255,6 +248,53 @@ fun AboutPixelixComposable( ) } } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 16.dp) + ) { + Text(text = "Konstantin Tskhovrebov", fontWeight = FontWeight.Bold) + + Row { + Image( + painter = painterResource(Res.drawable.pixelfed_logo), + contentDescription = null, + Modifier + .width(32.dp) + .height(32.dp) + .clickable { + navController.navigate(Destination.ProfileByUsername("dagboek@pixey.org")) + }) + + Spacer(modifier = Modifier.width(16.dp)) + + Image( + painter = painterResource(Res.drawable.mastodon_logo), + contentDescription = null, + Modifier + .width(32.dp) + .height(32.dp) + .clickable { + viewModel.openUrl("https://androiddev.social/@terrakok") + }) + + Spacer(modifier = Modifier.width(16.dp)) + + Icon( + imageVector = Icons.Outlined.Language, + contentDescription = "", + Modifier + .size(32.dp) + .clickable { + viewModel.openUrl("https://github.com/terrakok") + }, + tint = Color(0xFF4793FF) + ) + } + } } } } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/blocked_accounts/CustomBlockedAccountRow.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/blocked_accounts/CustomBlockedAccountRow.kt index e2a89f97..06dd17ce 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/blocked_accounts/CustomBlockedAccountRow.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/blocked_accounts/CustomBlockedAccountRow.kt @@ -20,7 +20,7 @@ import androidx.navigation.NavController import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.domain.model.Account import com.daniebeler.pfpixelix.ui.composables.profile.other_profile.UnBlockAccountAlert -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res @@ -39,7 +39,7 @@ fun CustomBlockedAccountRow( ) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { - Navigate.navigate("profile_screen/" + account.id, navController) + navController.navigate(Destination.Profile(account.id)) }) { AsyncImage( model = account.avatar, diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/followed_hashtags/FollowedHashtagsComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/followed_hashtags/FollowedHashtagsComposable.kt index b8db2e69..bf069fa8 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/followed_hashtags/FollowedHashtagsComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/followed_hashtags/FollowedHashtagsComposable.kt @@ -30,7 +30,7 @@ import com.daniebeler.pfpixelix.ui.composables.states.EmptyState import com.daniebeler.pfpixelix.ui.composables.states.FullscreenEmptyStateComposable import com.daniebeler.pfpixelix.ui.composables.states.FullscreenErrorComposable import com.daniebeler.pfpixelix.ui.composables.states.FullscreenLoadingComposable -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res @@ -89,7 +89,7 @@ fun FollowedHashtagsComposable( message = "Followed hashtags will appear here", buttonText = "Explore trending hashtags", onClick = { - Navigate.navigate("search_screen/2", navController) + navController.navigate(Destination.Search(2)) }) ) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/followed_hashtags/FollowedHashtagsViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/followed_hashtags/FollowedHashtagsViewModel.kt index aa4df17e..f3bbeab5 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/followed_hashtags/FollowedHashtagsViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/followed_hashtags/FollowedHashtagsViewModel.kt @@ -56,11 +56,7 @@ class FollowedHashtagsViewModel @Inject constructor( searchService.getHashtag(tag.name).onEach { result -> followedHashtagsState = when (result) { is Resource.Success -> { - if (result.data != null) { - FollowedHashtagsState(followedHashtags = followedHashtagsState.followedHashtags + result.data) - } else { - FollowedHashtagsState(followedHashtags = followedHashtagsState.followedHashtags) - } + FollowedHashtagsState(followedHashtags = followedHashtagsState.followedHashtags + result.data) } is Resource.Error -> { 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 091b5f77..280fde85 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 @@ -2,7 +2,6 @@ package com.daniebeler.pfpixelix.ui.composables.settings.icon_selection import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -34,21 +33,16 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.daniebeler.pfpixelix.di.injectViewModel -import com.daniebeler.pfpixelix.utils.LocalKmpContext import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/muted_accounts/CustomMutedAccountRow.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/muted_accounts/CustomMutedAccountRow.kt index d16fdea1..8ea45225 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/muted_accounts/CustomMutedAccountRow.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/muted_accounts/CustomMutedAccountRow.kt @@ -20,7 +20,7 @@ import androidx.navigation.NavController import coil3.compose.AsyncImage import com.daniebeler.pfpixelix.domain.model.Account import com.daniebeler.pfpixelix.ui.composables.profile.other_profile.UnMuteAccountAlert -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res @@ -41,7 +41,7 @@ fun CustomMutedAccountRow( Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { - Navigate.navigate("profile_screen/" + account.id, navController) + navController.navigate(Destination.Profile(account.id)) } ) { AsyncImage( 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 89cc04b3..8db93f34 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 @@ -21,10 +21,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -32,8 +32,10 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.domain.service.platform.PlatformFeatures +import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.AutoplayVideoPref import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.ClearCachePref import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.CustomizeAppIconPref +import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.DeleteAccountPref import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.FocusModePref import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.HideAltTextButtonPref import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.HideSensitiveContentPref @@ -42,7 +44,6 @@ import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.MoreSe import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.RepostSettingsPref import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.ThemePref import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.UseInAppBrowserPref -import com.daniebeler.pfpixelix.utils.LocalKmpContext import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res @@ -96,14 +97,20 @@ fun PreferencesComposable( UseInAppBrowserPref() } + if (PlatformFeatures.autoplayVideosPref) { + AutoplayVideoPref() + } + RepostSettingsPref { viewModel.openRepostSettings() } HorizontalDivider(modifier = Modifier.padding(12.dp)) ThemePref() - val icon = viewModel.appIcon.collectAsState() - CustomizeAppIconPref(navController, closePreferencesDrawer, icon.value) + if (PlatformFeatures.customAppIcon) { + val icon = viewModel.appIcon.collectAsState() + CustomizeAppIconPref(navController, closePreferencesDrawer, icon.value) + } HorizontalDivider(modifier = Modifier.padding(12.dp)) @@ -113,6 +120,8 @@ fun PreferencesComposable( LogoutPref { viewModel.logout() } + DeleteAccountPref { viewModel.openDeleteAccountPage() } + HorizontalDivider(modifier = Modifier.padding(12.dp)) Text( diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/PreferencesViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/PreferencesViewModel.kt index 27a06942..a402073f 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/PreferencesViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/PreferencesViewModel.kt @@ -1,5 +1,6 @@ package com.daniebeler.pfpixelix.ui.composables.settings.preferences +import androidx.compose.ui.platform.UriHandler import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.daniebeler.pfpixelix.domain.service.icon.AppIconService @@ -34,4 +35,10 @@ class PreferencesViewModel( platform.openUrl("${it.serverUrl}settings/timeline") } } + + fun openDeleteAccountPage() { + authService.getCurrentSession()?.let { + platform.openUrl("${it.serverUrl}settings/remove/request/permanent") + } + } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/AutoplayVideoPref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/AutoplayVideoPref.kt new file mode 100644 index 00000000..3cd4649a --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/AutoplayVideoPref.kt @@ -0,0 +1,26 @@ +package com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.daniebeler.pfpixelix.di.LocalAppComponent +import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.SwitchPref +import org.jetbrains.compose.resources.stringResource +import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.autoplay_videos +import pixelix.app.generated.resources.square_outline + +@Composable +fun AutoplayVideoPref() { + val prefs = LocalAppComponent.current.preferences + val state = remember { mutableStateOf(prefs.autoplayVideo) } + LaunchedEffect(state.value) { + prefs.autoplayVideo = state.value + } + SwitchPref( + leadingIcon = Res.drawable.square_outline, + title = stringResource(Res.string.autoplay_videos), + state = state + ) +} \ No newline at end of file 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 24cd9ecf..24320948 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,15 @@ package com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.prefs import androidx.lifecycle.ViewModel -import com.daniebeler.pfpixelix.domain.service.platform.Platform +import com.daniebeler.pfpixelix.domain.service.file.FileService import me.tatarka.inject.annotations.Inject @Inject class ClearCacheViewModel( - private val platform: Platform + private val fileService: FileService ): ViewModel() { - fun getCacheSizeInBytes() = platform.getCacheSizeInBytes() + fun getCacheSizeInBytes() = fileService.getCacheSizeInBytes() fun cleanCache() { - platform.cleanCache() + fileService.cleanCache() } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/CustomAccentColorPref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/CustomAccentColorPref.kt new file mode 100644 index 00000000..65ffbc83 --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/CustomAccentColorPref.kt @@ -0,0 +1,68 @@ +package com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.prefs + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.unit.dp +import com.daniebeler.pfpixelix.di.LocalAppComponent +import com.daniebeler.pfpixelix.domain.model.AppAccentColor + +@Composable +fun CustomAccentColorPref() { + val prefs = LocalAppComponent.current.preferences + val state = remember { mutableStateOf(prefs.accentColor) } + LaunchedEffect(state.value) { + prefs.accentColor = state.value + } + + Card( + shape = MaterialTheme.shapes.medium, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .defaultMinSize(minHeight = 48.dp) + .padding(start = 12.dp, top = 8.dp, end = 12.dp, bottom = 8.dp) + ) { + Row(Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceEvenly) { + Box(Modifier.height(32.dp).width(32.dp).clip(CircleShape).background(Color(AppAccentColor.GREEN)).clickable { + state.value = AppAccentColor.GREEN + }) + Box(Modifier.height(32.dp).width(32.dp).clip(CircleShape).background(Color(AppAccentColor.RED)).clickable { + state.value = AppAccentColor.RED + }) + Box(Modifier.height(32.dp).width(32.dp).clip(CircleShape).background(Color(AppAccentColor.BLUE)).clickable { + state.value = AppAccentColor.BLUE + }) + val dark = MaterialTheme.colorScheme.background.luminance() < 0.5 + Box(Modifier.height(32.dp).width(32.dp).clip(CircleShape).background(if (dark) {Color(AppAccentColor.White)} else {Color(0xFF5D5F5F)}).clickable { + state.value = AppAccentColor.White + })} + } + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/CustomizeAppIconPref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/CustomizeAppIconPref.kt index c67d7045..045d3297 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/CustomizeAppIconPref.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/CustomizeAppIconPref.kt @@ -1,18 +1,15 @@ package com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.ImageBitmap import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.SettingPref -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.chevron_forward_outline import pixelix.app.generated.resources.customize_app_icon -import pixelix.app.generated.resources.pixelix_logo @Composable fun CustomizeAppIconPref(navController: NavController, closePreferenceDrawer: () -> Unit, logo: DrawableResource) { @@ -21,6 +18,6 @@ fun CustomizeAppIconPref(navController: NavController, closePreferenceDrawer: () trailingContent = Res.drawable.chevron_forward_outline, onClick = { closePreferenceDrawer() - Navigate.navigate("icon_selection_screen", navController) + navController.navigate(Destination.IconSelection) }) } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/DeleteAccountPref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/DeleteAccountPref.kt new file mode 100644 index 00000000..cb5fd215 --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/DeleteAccountPref.kt @@ -0,0 +1,22 @@ +package com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.SettingPref +import org.jetbrains.compose.resources.stringResource +import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.open_outline +import pixelix.app.generated.resources.trash +import pixelix.app.generated.resources.delete_account + + +@Composable +fun DeleteAccountPref(openUrl: () -> Unit) { + SettingPref( + leadingIcon = Res.drawable.trash, + title = stringResource(Res.string.delete_account), + trailingContent = Res.drawable.open_outline, + onClick = openUrl, + textColor = MaterialTheme.colorScheme.error + ) +} diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/HideSensitiveContentPref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/HideSensitiveContentPref.kt index 2242002a..856cea48 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/HideSensitiveContentPref.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/HideSensitiveContentPref.kt @@ -1,28 +1,62 @@ package com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.BlurOn +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.daniebeler.pfpixelix.di.LocalAppComponent import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.SwitchPref import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.eye_off_outline +import pixelix.app.generated.resources.blur import pixelix.app.generated.resources.hide_sensitive_content +import pixelix.app.generated.resources.blur_sensitive_content @Composable fun HideSensitiveContentPref() { val prefs = LocalAppComponent.current.preferences - val state = remember { mutableStateOf(prefs.hideSensitiveContent) } - LaunchedEffect(state.value) { - prefs.hideSensitiveContent = state.value + val hideState = remember { mutableStateOf(prefs.hideSensitiveContent) } + LaunchedEffect(hideState.value) { + prefs.hideSensitiveContent = hideState.value + } + + val blurState = remember { mutableStateOf(prefs.blurSensitiveContent) } + LaunchedEffect(blurState.value) { + prefs.blurSensitiveContent = blurState.value + } + Column { + SwitchPref( + leadingIcon = Res.drawable.eye_off_outline, + title = stringResource(Res.string.hide_sensitive_content), + state = hideState + ) + + AnimatedVisibility( + modifier = Modifier.padding(top = 8.dp), + visible = !hideState.value, + enter = slideInVertically() + fadeIn(), + exit = shrinkVertically(animationSpec = spring(stiffness = Spring.StiffnessMedium)) + fadeOut(), + ) { + SwitchPref( + leadingIcon = Res.drawable.blur, + title = stringResource(Res.string.blur_sensitive_content), + state = blurState + ) + } } - SwitchPref( - leadingIcon = Res.drawable.eye_off_outline, - title = stringResource(Res.string.hide_sensitive_content), - state = state - ) } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/LogoutPref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/LogoutPref.kt index c76e5218..ffdc7828 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/LogoutPref.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/LogoutPref.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.SettingPref -import com.daniebeler.pfpixelix.utils.LocalKmpContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -38,7 +37,6 @@ fun LogoutPref(logout: () -> Unit) { @Composable fun LogoutAlert(show: MutableState, logout: () -> Unit) { - val context = LocalKmpContext.current if (show.value) { AlertDialog(title = { Text(text = stringResource(Res.string.logout_questionmark)) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/MoreSettingsPref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/MoreSettingsPref.kt index 51306b34..d60e23cb 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/MoreSettingsPref.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/MoreSettingsPref.kt @@ -1,6 +1,7 @@ package com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs import androidx.compose.runtime.Composable +import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.SettingPref import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res @@ -21,6 +22,6 @@ fun MoreSettingsPref(openUrl: () -> Unit) { @Composable private fun MoreSettingsPrefPreview() { MoreSettingsPref(openUrl = { - println("URL opened: url") + Logger.v("URL opened: url") }) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/RepostSettingsPref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/RepostSettingsPref.kt index da96ec3f..7325410a 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/RepostSettingsPref.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/RepostSettingsPref.kt @@ -18,10 +18,3 @@ fun RepostSettingsPref(openUrl: () -> Unit) { onClick = openUrl ) } - -@Composable -private fun RepostSettingsPrefPreview() { - RepostSettingsPref(openUrl = { - println("URL opened: url") - }) -} diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ThemePref.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ThemePref.kt index b8fd3b9b..7563d243 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ThemePref.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/settings/preferences/prefs/prefs/ThemePref.kt @@ -1,5 +1,7 @@ package com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.BrightnessAuto import androidx.compose.material.icons.rounded.Contrast @@ -8,16 +10,20 @@ import androidx.compose.material.icons.rounded.LightMode import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.daniebeler.pfpixelix.di.LocalAppComponent import com.daniebeler.pfpixelix.domain.model.AppThemeMode.AMOLED import com.daniebeler.pfpixelix.domain.model.AppThemeMode.DARK import com.daniebeler.pfpixelix.domain.model.AppThemeMode.FOLLOW_SYSTEM import com.daniebeler.pfpixelix.domain.model.AppThemeMode.LIGHT +import com.daniebeler.pfpixelix.domain.service.platform.PlatformFeatures import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.ExpandOptionsPref import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.OptionShapes import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.ValueOption import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.imageVectorIconBlock import com.daniebeler.pfpixelix.ui.composables.settings.preferences.basic.radioButtonBlock +import com.daniebeler.pfpixelix.ui.composables.settings.preferences.prefs.prefs.CustomAccentColorPref import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.amoled @@ -85,5 +91,10 @@ fun ThemePref() { onOptionClick = onOptionClick, ) + if (PlatformFeatures.customAccentColors) { + Spacer(modifier = Modifier.height(1.dp)) + + CustomAccentColorPref() + } } } \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/single_post/SinglePostComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/single_post/SinglePostComposable.kt index 3d17393a..9701580c 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/single_post/SinglePostComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/single_post/SinglePostComposable.kt @@ -27,7 +27,7 @@ import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.ui.composables.post.PostComposable import com.daniebeler.pfpixelix.ui.composables.states.ErrorComposable import com.daniebeler.pfpixelix.ui.composables.states.LoadingComposable -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.vectorResource import pixelix.app.generated.resources.Res @@ -87,7 +87,9 @@ fun SinglePostComposable( Column(modifier = Modifier.verticalScroll(scrollState)) { if (viewModel.postState.post != null) { PostComposable(viewModel.postState.post!!, navController, postGetsDeleted = { - Navigate.navigateAndDeleteBackStack("own_profile_screen", navController) + navController.navigate(Destination.OwnProfile) { + popUpTo(0) { inclusive = true } + } }, setZindex = { }, openReplies) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/states/ErrorComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/states/ErrorComposable.kt index 612656bc..ff3c3d7a 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/states/ErrorComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/states/ErrorComposable.kt @@ -11,10 +11,17 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -30,6 +37,23 @@ fun ErrorComposable(message: String) { } } +@Composable +fun ErrorComposableDialog(errorMessage: String?, onDismiss: () -> Unit) { + if (!errorMessage.isNullOrBlank()) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Error") }, + text = { Text(errorMessage) }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text("OK") + } + } + ) + } +} + + @Composable fun FullscreenErrorComposable(message: String) { if (message.isNotBlank()) { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/textfield_location/TextFieldLocationsViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/textfield_location/TextFieldLocationsViewModel.kt index 25feecd3..ff6f75cf 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/textfield_location/TextFieldLocationsViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/textfield_location/TextFieldLocationsViewModel.kt @@ -39,13 +39,7 @@ class TextFieldLocationsViewModel @Inject constructor( searchService.searchLocations(location).onEach { result -> locationsSuggestions = when (result) { is Resource.Success -> { - if (result.data != null) { - LocationsState(locations = result.data) - } else { - LocationsState( - error = result.message ?: "An unexpected error occurred" - ) - } + LocationsState(locations = result.data) } is Resource.Error -> { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/textfield_mentions/TextFieldMentionsViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/textfield_mentions/TextFieldMentionsViewModel.kt index 1dd6e3d8..2295030c 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/textfield_mentions/TextFieldMentionsViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/textfield_mentions/TextFieldMentionsViewModel.kt @@ -46,13 +46,7 @@ class TextFieldMentionsViewModel @Inject constructor( searchService.search(searchShortened).onEach { result -> mentionSuggestions = when (result) { is Resource.Success -> { - if (result.data != null) { - SuggestionsState(suggestions = if (type == "accounts") {result.data.accounts.map { "@" + it.acct }} else {result.data.tags.map { "#" + it.name }}) - } else { - SuggestionsState( - error = result.message ?: "An unexpected error occurred" - ) - } + SuggestionsState(suggestions = if (type == "accounts") {result.data.accounts.map { "@" + it.acct }} else {result.data.tags.map { "#" + it.name }}) } is Resource.Error -> { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/global_timeline/GlobalTimelineViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/global_timeline/GlobalTimelineViewModel.kt index 4d3112ad..90a48110 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/global_timeline/GlobalTimelineViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/global_timeline/GlobalTimelineViewModel.kt @@ -23,6 +23,9 @@ class GlobalTimelineViewModel @Inject constructor( } private fun getItemsFirstLoad(refreshing: Boolean) { + if (globalTimelineState.globalTimeline.isNotEmpty() && !refreshing) { + return + } timelineService.getGlobalTimeline().onEach { result -> globalTimelineState = when (result) { is Resource.Success -> { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/hashtag_timeline/HashtagTimelineComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/hashtag_timeline/HashtagTimelineComposable.kt index ed8a9152..35e1a1cd 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/hashtag_timeline/HashtagTimelineComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/hashtag_timeline/HashtagTimelineComposable.kt @@ -1,35 +1,22 @@ package com.daniebeler.pfpixelix.ui.composables.timelines.hashtag_timeline -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.outlined.Photo -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -37,26 +24,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.ui.composables.FollowButton -import com.daniebeler.pfpixelix.ui.composables.InfiniteListHandler -import com.daniebeler.pfpixelix.ui.composables.InfinitePostsGrid import com.daniebeler.pfpixelix.ui.composables.InfinitePostsList -import com.daniebeler.pfpixelix.ui.composables.ToTopButton -import com.daniebeler.pfpixelix.ui.composables.profile.AccountState -import com.daniebeler.pfpixelix.ui.composables.profile.PostsState -import com.daniebeler.pfpixelix.ui.composables.profile.PostsWrapperComposable -import com.daniebeler.pfpixelix.ui.composables.profile.SwitchViewComposable -import com.daniebeler.pfpixelix.ui.composables.profile.ViewEnum -import com.daniebeler.pfpixelix.ui.composables.states.EmptyState -import com.daniebeler.pfpixelix.utils.Navigate -import com.daniebeler.pfpixelix.utils.StringFormat -import org.jetbrains.compose.resources.stringResource -import pixelix.app.generated.resources.Res -import pixelix.app.generated.resources.posts @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/hashtag_timeline/HashtagTimelineViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/hashtag_timeline/HashtagTimelineViewModel.kt index beba551a..93109587 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/hashtag_timeline/HashtagTimelineViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/hashtag_timeline/HashtagTimelineViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.domain.model.Post import com.daniebeler.pfpixelix.domain.model.RelatedHashtag import com.daniebeler.pfpixelix.domain.repository.PixelfedApi @@ -53,6 +54,9 @@ class HashtagTimelineViewModel @Inject constructor( } fun getItemsFirstLoad(hashtag: String, refreshing: Boolean = false) { + if (postsState.hashtagTimeline.isNotEmpty() && !refreshing) { + return + } timelineService.getHashtagTimeline(hashtag).onEach { result -> postsState = when (result) { is Resource.Success -> { @@ -132,10 +136,10 @@ class HashtagTimelineViewModel @Inject constructor( fun getRelatedHashtags(hashtag: String) { searchService.getRelatedHashtags(hashtag).onEach { result -> if (result is Resource.Success) { - relatedHashtags = result.data ?: emptyList() - println("juhuu" + result.data) + relatedHashtags = result.data + Logger.v("juhuu" + result.data) } else { - println("fief" + result.message) + Logger.v("fief" + result.message) } }.launchIn(viewModelScope) } diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/home_timeline/HomeTimelineComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/home_timeline/HomeTimelineComposable.kt index 05ca0bec..4334c006 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/home_timeline/HomeTimelineComposable.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/home_timeline/HomeTimelineComposable.kt @@ -9,9 +9,8 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavController import com.daniebeler.pfpixelix.di.injectViewModel import com.daniebeler.pfpixelix.ui.composables.InfinitePostsList -import com.daniebeler.pfpixelix.ui.composables.profile.ViewEnum import com.daniebeler.pfpixelix.ui.composables.states.EmptyState -import com.daniebeler.pfpixelix.utils.Navigate +import com.daniebeler.pfpixelix.ui.navigation.Destination import org.jetbrains.compose.resources.stringResource import pixelix.app.generated.resources.Res import pixelix.app.generated.resources.explore_trending_profiles @@ -35,7 +34,7 @@ fun HomeTimelineComposable( message = stringResource(Res.string.follow_accounts_or_hashtags_to_fill_your_home_timeline), buttonText = stringResource(Res.string.explore_trending_profiles), onClick = { - Navigate.navigate("search_screen/1", navController) + navController.navigate(Destination.Search(1)) }), getItemsPaginated = { viewModel.getItemsPaginated() diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/home_timeline/HomeTimelineViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/home_timeline/HomeTimelineViewModel.kt index b30b5a02..8e2e458c 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/home_timeline/HomeTimelineViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/home_timeline/HomeTimelineViewModel.kt @@ -48,6 +48,9 @@ class HomeTimelineViewModel @Inject constructor( } private fun getItemsFirstLoad(refreshing: Boolean, enableReblogs: Boolean) { + if (homeTimelineState.homeTimeline.isNotEmpty() && !refreshing) { + return + } timelineService.getHomeTimeline( enableReblogs = enableReblogs ).onEach { result -> diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/local_timeline/LocalTimelineViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/local_timeline/LocalTimelineViewModel.kt index abe0de38..2a7ad6c1 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/local_timeline/LocalTimelineViewModel.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/timelines/local_timeline/LocalTimelineViewModel.kt @@ -23,6 +23,9 @@ class LocalTimelineViewModel @Inject constructor( } private fun getItemsFirstLoad(refreshing: Boolean) { + if (localTimelineState.localTimeline.isNotEmpty() && !refreshing) { + return + } timelineService.getLocalTimeline().onEach { result -> localTimelineState = when (result) { is Resource.Success -> { diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/navigation/Navigation.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/navigation/Navigation.kt new file mode 100644 index 00000000..b31fc873 --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/navigation/Navigation.kt @@ -0,0 +1,290 @@ +package com.daniebeler.pfpixelix.ui.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.compose.dialog +import androidx.navigation.compose.navigation +import androidx.navigation.toRoute +import com.daniebeler.pfpixelix.EdgeToEdgeDialogProperties +import com.daniebeler.pfpixelix.ui.composables.HomeComposable +import com.daniebeler.pfpixelix.ui.composables.collection.CollectionComposable +import com.daniebeler.pfpixelix.ui.composables.direct_messages.chat.ChatComposable +import com.daniebeler.pfpixelix.ui.composables.direct_messages.conversations.ConversationsComposable +import com.daniebeler.pfpixelix.ui.composables.edit_post.EditPostComposable +import com.daniebeler.pfpixelix.ui.composables.edit_profile.EditProfileComposable +import com.daniebeler.pfpixelix.ui.composables.explore.ExploreComposable +import com.daniebeler.pfpixelix.ui.composables.followers.FollowersMainComposable +import com.daniebeler.pfpixelix.ui.composables.mention.MentionComposable +import com.daniebeler.pfpixelix.ui.composables.newpost.NewPostComposable +import com.daniebeler.pfpixelix.ui.composables.notifications.NotificationsComposable +import com.daniebeler.pfpixelix.ui.composables.profile.other_profile.OtherProfileComposable +import com.daniebeler.pfpixelix.ui.composables.profile.own_profile.OwnProfileComposable +import com.daniebeler.pfpixelix.ui.composables.session.LoginComposable +import com.daniebeler.pfpixelix.ui.composables.settings.about_instance.AboutInstanceComposable +import com.daniebeler.pfpixelix.ui.composables.settings.about_pixelix.AboutPixelixComposable +import com.daniebeler.pfpixelix.ui.composables.settings.blocked_accounts.BlockedAccountsComposable +import com.daniebeler.pfpixelix.ui.composables.settings.bookmarked_posts.BookmarkedPostsComposable +import com.daniebeler.pfpixelix.ui.composables.settings.followed_hashtags.FollowedHashtagsComposable +import com.daniebeler.pfpixelix.ui.composables.settings.icon_selection.IconSelectionComposable +import com.daniebeler.pfpixelix.ui.composables.settings.liked_posts.LikedPostsComposable +import com.daniebeler.pfpixelix.ui.composables.settings.muted_accounts.MutedAccountsComposable +import com.daniebeler.pfpixelix.ui.composables.single_post.SinglePostComposable +import com.daniebeler.pfpixelix.ui.composables.timelines.hashtag_timeline.HashtagTimelineComposable +import com.daniebeler.pfpixelix.utils.KmpUri +import com.daniebeler.pfpixelix.utils.toKmpUri +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmSuppressWildcards + +sealed interface Destination { + @Serializable data class Hashtag(val hashtag: String) : Destination + @Serializable data class HashtagTimeline(val hashtag: String) : Destination + @Serializable data class Post( + val id: String, + val refresh: Boolean = false, + val openReplies: Boolean = false + ) : Destination + @Serializable data class EditPost(val id: String) : Destination + @Serializable data class Collection(val id: String) : Destination + @Serializable data class Followers(val userId: String, val isFollowers: Boolean) : Destination + @Serializable data object Conversations : Destination + @Serializable data class Chat(val id: String) : Destination + @Serializable data class Mention(val id: String) : Destination + @Serializable data object EditProfile : Destination + @Serializable data object IconSelection : Destination + @Serializable data object MutedAccounts : Destination + @Serializable data object BlockedAccounts : Destination + @Serializable data object LikedPosts : Destination + @Serializable data object BookmarkedPosts : Destination + @Serializable data object FollowedHashtags : Destination + @Serializable data object AboutInstance : Destination + @Serializable data object AboutPixelix : Destination + @Serializable data class Profile(val userId: String) : Destination + @Serializable data class ProfileByUsername(val userName: String) : Destination + @Serializable data object FirstLogin : Destination + @Serializable data object NewLogin : Destination + @Serializable data class Search(val page: Int = 0) : Destination + @Serializable data object OwnProfile : Destination + @Serializable data object Feeds : Destination + @Serializable data class NewPost(val uris: List = emptyList()) : Destination + @Serializable data object Notifications : Destination + + @Serializable data object HomeTabFeeds : Destination + @Serializable data object HomeTabSearch : Destination + @Serializable data object HomeTabNewPost : Destination + @Serializable data object HomeTabNotifications : Destination + @Serializable data object HomeTabOwnProfile : Destination +} + +internal fun NavGraphBuilder.appGraph( + navController: NavHostController, + openPreferencesDrawer: () -> Unit, + exitApp: () -> Unit +) { + composable( + enterTransition = { EnterTransition.None }, + exitTransition = { ExitTransition.None } + ) { + Dialog( + onDismissRequest = exitApp, + properties = EdgeToEdgeDialogProperties() + ) { + LoginComposable(navController = navController) + } + } + + //home tabs (with no transition animations) + //more info: https://issuetracker.google.com/408010634 + navigation( + startDestination = Destination.Feeds, + enterTransition = { tabEnterTransition() }, + exitTransition = { tabExitTransition() } + ) { + tabGraph(navController, openPreferencesDrawer) + } + + navigation( + startDestination = Destination.Search(), + enterTransition = { tabEnterTransition() }, + exitTransition = { tabExitTransition() } + ) { + tabGraph(navController, openPreferencesDrawer) + } + + navigation( + startDestination = Destination.NewPost(), + enterTransition = { tabEnterTransition() }, + exitTransition = { tabExitTransition() } + ) { + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + val imageUris: List? = args.uris.map { it.toKmpUri() } + NewPostComposable(navController, imageUris) + } + + tabGraph(navController, openPreferencesDrawer) + } + + navigation( + startDestination = Destination.Notifications, + enterTransition = { tabEnterTransition() }, + exitTransition = { tabExitTransition() } + ) { + tabGraph(navController, openPreferencesDrawer) + } + + navigation( + startDestination = Destination.OwnProfile, + enterTransition = { tabEnterTransition() }, + exitTransition = { tabExitTransition() } + ) { + tabGraph(navController, openPreferencesDrawer) + } +} + +private inline fun AnimatedContentTransitionScope.tabEnterTransition(): EnterTransition? { + val initialHierarchy = initialState.destination.hierarchy + return if (initialHierarchy.none { it.hasRoute() }) EnterTransition.None else null +} + +private inline fun AnimatedContentTransitionScope.tabExitTransition(): ExitTransition? { + val targetHierarchy = targetState.destination.hierarchy + return if (targetHierarchy.none { it.hasRoute() }) ExitTransition.None else null +} + +private fun NavGraphBuilder.tabGraph( + navController: NavHostController, + openPreferencesDrawer: () -> Unit +) { + dialog( + dialogProperties = EdgeToEdgeDialogProperties() + ) { + LoginComposable(true, navController) + } + + composable { + HomeComposable(navController, openPreferencesDrawer) + } + + composable { + NotificationsComposable(navController) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + HashtagTimelineComposable(navController, args.hashtag) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + OtherProfileComposable(navController, userId = args.userId, byUsername = null) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + OtherProfileComposable(navController, userId = "", byUsername = args.userName) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + HashtagTimelineComposable(navController, args.hashtag) + } + + composable { + EditProfileComposable(navController) + } + + composable { + IconSelectionComposable(navController) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + EditPostComposable(args.id, navController) + } + + composable { + MutedAccountsComposable(navController) + } + + composable { + BlockedAccountsComposable(navController) + } + + composable { + LikedPostsComposable(navController) + } + + composable { + BookmarkedPostsComposable(navController) + } + + composable { + FollowedHashtagsComposable(navController) + } + + composable { + AboutInstanceComposable(navController) + } + + composable { + AboutPixelixComposable(navController) + } + + composable { + OwnProfileComposable(navController, openPreferencesDrawer) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + FollowersMainComposable( + navController, + accountId = args.userId, + isFollowers = args.isFollowers + ) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + SinglePostComposable(navController, postId = args.id, args.refresh, args.openReplies) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + CollectionComposable(navController, collectionId = args.id) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + ExploreComposable(navController, args.page) + } + + composable { + ConversationsComposable(navController = navController) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + ChatComposable(navController = navController, accountId = args.id) + } + + composable { navBackStackEntry -> + val args = navBackStackEntry.toRoute() + MentionComposable(navController = navController, mentionId = args.id) + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/ColorBlue.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/ColorBlue.kt new file mode 100644 index 00000000..0d90cfab --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/ColorBlue.kt @@ -0,0 +1,74 @@ +package com.daniebeler.pfpixelix.ui.theme +import androidx.compose.ui.graphics.Color + +val bluePrimaryLight = Color(0xFF4C5C92) +val blueOnPrimaryLight = Color(0xFFFFFFFF) +val bluePrimaryContainerLight = Color(0xFFDBE1FF) +val blueOnPrimaryContainerLight = Color(0xFF334479) +val blueSecondaryLight = Color(0xFF595E72) +val blueOnSecondaryLight = Color(0xFFFFFFFF) +val blueSecondaryContainerLight = Color(0xFFDDE1F9) +val blueOnSecondaryContainerLight = Color(0xFF414659) +val blueTertiaryLight = Color(0xFF745470) +val blueOnTertiaryLight = Color(0xFFFFFFFF) +val blueTertiaryContainerLight = Color(0xFFFFD6F7) +val blueOnTertiaryContainerLight = Color(0xFF5B3D57) +val blueErrorLight = Color(0xFFBA1A1A) +val blueOnErrorLight = Color(0xFFFFFFFF) +val blueErrorContainerLight = Color(0xFFFFDAD6) +val blueOnErrorContainerLight = Color(0xFF93000A) +val blueBackgroundLight = Color(0xFFFAF8FF) +val blueOnBackgroundLight = Color(0xFF1A1B21) +val blueSurfaceLight = Color(0xFFFAF8FF) +val blueOnSurfaceLight = Color(0xFF1A1B21) +val blueSurfaceVariantLight = Color(0xFFE2E1EC) +val blueOnSurfaceVariantLight = Color(0xFF45464F) +val blueOutlineLight = Color(0xFF767680) +val blueOutlineVariantLight = Color(0xFFC6C6D0) +val blueScrimLight = Color(0xFF000000) +val blueInverseSurfaceLight = Color(0xFF2F3036) +val blueInverseOnSurfaceLight = Color(0xFFF1F0F7) +val blueInversePrimaryLight = Color(0xFFB5C4FF) +val blueSurfaceDimLight = Color(0xFFDAD9E0) +val blueSurfaceBrightLight = Color(0xFFFAF8FF) +val blueSurfaceContainerLowestLight = Color(0xFFFFFFFF) +val blueSurfaceContainerLowLight = Color(0xFFF4F3FA) +val blueSurfaceContainerLight = Color(0xFFEEEDF4) +val blueSurfaceContainerHighLight = Color(0xFFE9E7EF) +val blueSurfaceContainerHighestLight = Color(0xFFE3E1E9) + +val bluePrimaryDark = Color(0xFFB5C4FF) +val blueOnPrimaryDark = Color(0xFF1C2D61) +val bluePrimaryContainerDark = Color(0xFF334479) +val blueOnPrimaryContainerDark = Color(0xFFDBE1FF) +val blueSecondaryDark = Color(0xFFC1C5DD) +val blueOnSecondaryDark = Color(0xFF2B3042) +val blueSecondaryContainerDark = Color(0xFF414659) +val blueOnSecondaryContainerDark = Color(0xFFDDE1F9) +val blueTertiaryDark = Color(0xFFE2BBDB) +val blueOnTertiaryDark = Color(0xFF432740) +val blueTertiaryContainerDark = Color(0xFF5B3D57) +val blueOnTertiaryContainerDark = Color(0xFFFFD6F7) +val blueErrorDark = Color(0xFFFFB4AB) +val blueOnErrorDark = Color(0xFF690005) +val blueErrorContainerDark = Color(0xFF93000A) +val blueOnErrorContainerDark = Color(0xFFFFDAD6) +val blueBackgroundDark = Color(0xFF121318) +val blueOnBackgroundDark = Color(0xFFE3E1E9) +val blueSurfaceDark = Color(0xFF121318) +val blueOnSurfaceDark = Color(0xFFE3E1E9) +val blueSurfaceVariantDark = Color(0xFF45464F) +val blueOnSurfaceVariantDark = Color(0xFFC6C6D0) +val blueOutlineDark = Color(0xFF8F909A) +val blueOutlineVariantDark = Color(0xFF45464F) +val blueScrimDark = Color(0xFF000000) +val blueInverseSurfaceDark = Color(0xFFE3E1E9) +val blueInverseOnSurfaceDark = Color(0xFF2F3036) +val blueInversePrimaryDark = Color(0xFF4C5C92) +val blueSurfaceDimDark = Color(0xFF121318) +val blueSurfaceBrightDark = Color(0xFF38393F) +val blueSurfaceContainerLowestDark = Color(0xFF0D0E13) +val blueSurfaceContainerLowDark = Color(0xFF1A1B21) +val blueSurfaceContainerDark = Color(0xFF1E1F25) +val blueSurfaceContainerHighDark = Color(0xFF292A2F) +val blueSurfaceContainerHighestDark = Color(0xFF34343A) \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/ColorRed.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/ColorRed.kt new file mode 100644 index 00000000..1c9263b9 --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/ColorRed.kt @@ -0,0 +1,74 @@ +package com.daniebeler.pfpixelix.ui.theme +import androidx.compose.ui.graphics.Color + +val redPrimaryLight = Color(0xFF8F4C38) +val redOnPrimaryLight = Color(0xFFFFFFFF) +val redPrimaryContainerLight = Color(0xFFFFDBD1) +val redOnPrimaryContainerLight = Color(0xFF723523) +val redSecondaryLight = Color(0xFF77574E) +val redOnSecondaryLight = Color(0xFFFFFFFF) +val redSecondaryContainerLight = Color(0xFFFFDBD1) +val redOnSecondaryContainerLight = Color(0xFF5D4037) +val redTertiaryLight = Color(0xFF6C5D2F) +val redOnTertiaryLight = Color(0xFFFFFFFF) +val redTertiaryContainerLight = Color(0xFFF5E1A7) +val redOnTertiaryContainerLight = Color(0xFF534619) +val redErrorLight = Color(0xFFBA1A1A) +val redOnErrorLight = Color(0xFFFFFFFF) +val redErrorContainerLight = Color(0xFFFFDAD6) +val redOnErrorContainerLight = Color(0xFF93000A) +val redBackgroundLight = Color(0xFFFFF8F6) +val redOnBackgroundLight = Color(0xFF231917) +val redSurfaceLight = Color(0xFFFFF8F6) +val redOnSurfaceLight = Color(0xFF231917) +val redSurfaceVariantLight = Color(0xFFF5DED8) +val redOnSurfaceVariantLight = Color(0xFF53433F) +val redOutlineLight = Color(0xFF85736E) +val redOutlineVariantLight = Color(0xFFD8C2BC) +val redScrimLight = Color(0xFF000000) +val redInverseSurfaceLight = Color(0xFF392E2B) +val redInverseOnSurfaceLight = Color(0xFFFFEDE8) +val redInversePrimaryLight = Color(0xFFFFB5A0) +val redSurfaceDimLight = Color(0xFFE8D6D2) +val redSurfaceBrightLight = Color(0xFFFFF8F6) +val redSurfaceContainerLowestLight = Color(0xFFFFFFFF) +val redSurfaceContainerLowLight = Color(0xFFFFF1ED) +val redSurfaceContainerLight = Color(0xFFFCEAE5) +val redSurfaceContainerHighLight = Color(0xFFF7E4E0) +val redSurfaceContainerHighestLight = Color(0xFFF1DFDA) + +val redPrimaryDark = Color(0xFFFFB5A0) +val redOnPrimaryDark = Color(0xFF561F0F) +val redPrimaryContainerDark = Color(0xFF723523) +val redOnPrimaryContainerDark = Color(0xFFFFDBD1) +val redSecondaryDark = Color(0xFFE7BDB2) +val redOnSecondaryDark = Color(0xFF442A22) +val redSecondaryContainerDark = Color(0xFF5D4037) +val redOnSecondaryContainerDark = Color(0xFFFFDBD1) +val redTertiaryDark = Color(0xFFD8C58D) +val redOnTertiaryDark = Color(0xFF3B2F05) +val redTertiaryContainerDark = Color(0xFF534619) +val redOnTertiaryContainerDark = Color(0xFFF5E1A7) +val redErrorDark = Color(0xFFFFB4AB) +val redOnErrorDark = Color(0xFF690005) +val redErrorContainerDark = Color(0xFF93000A) +val redOnErrorContainerDark = Color(0xFFFFDAD6) +val redBackgroundDark = Color(0xFF1A110F) +val redOnBackgroundDark = Color(0xFFF1DFDA) +val redSurfaceDark = Color(0xFF1A110F) +val redOnSurfaceDark = Color(0xFFF1DFDA) +val redSurfaceVariantDark = Color(0xFF53433F) +val redOnSurfaceVariantDark = Color(0xFFD8C2BC) +val redOutlineDark = Color(0xFFA08C87) +val redOutlineVariantDark = Color(0xFF53433F) +val redScrimDark = Color(0xFF000000) +val redInverseSurfaceDark = Color(0xFFF1DFDA) +val redInverseOnSurfaceDark = Color(0xFF392E2B) +val redInversePrimaryDark = Color(0xFF8F4C38) +val redSurfaceDimDark = Color(0xFF1A110F) +val redSurfaceBrightDark = Color(0xFF423734) +val redSurfaceContainerLowestDark = Color(0xFF140C0A) +val redSurfaceContainerLowDark = Color(0xFF231917) +val redSurfaceContainerDark = Color(0xFF271D1B) +val redSurfaceContainerHighDark = Color(0xFF322825) +val redSurfaceContainerHighestDark = Color(0xFF3D322F) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/ColorWhite.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/ColorWhite.kt new file mode 100644 index 00000000..2c3abee9 --- /dev/null +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/ColorWhite.kt @@ -0,0 +1,74 @@ +package com.daniebeler.pfpixelix.ui.theme +import androidx.compose.ui.graphics.Color + +val whitePrimaryLight = Color(0xFF5D5F5F) +val whiteOnPrimaryLight = Color(0xFFFFFFFF) +val whitePrimaryContainerLight = Color(0xFFFFFFFF) +val whiteOnPrimaryContainerLight = Color(0xFF747676) +val whiteSecondaryLight = Color(0xFF5E5E5E) +val whiteOnSecondaryLight = Color(0xFFFFFFFF) +val whiteSecondaryContainerLight = Color(0xFFE4E2E2) +val whiteOnSecondaryContainerLight = Color(0xFF646464) +val whiteTertiaryLight = Color(0xFF5D5F5F) +val whiteOnTertiaryLight = Color(0xFFFFFFFF) +val whiteTertiaryContainerLight = Color(0xFFFFFFFF) +val whiteOnTertiaryContainerLight = Color(0xFF747676) +val whiteErrorLight = Color(0xFFBA1A1A) +val whiteOnErrorLight = Color(0xFFFFFFFF) +val whiteErrorContainerLight = Color(0xFFFFDAD6) +val whiteOnErrorContainerLight = Color(0xFF93000A) +val whiteBackgroundLight = Color(0xFFFCF8F8) +val whiteOnBackgroundLight = Color(0xFF1C1B1B) +val whiteSurfaceLight = Color(0xFFFCF8F8) +val whiteOnSurfaceLight = Color(0xFF1C1B1B) +val whiteSurfaceVariantLight = Color(0xFFE0E3E3) +val whiteOnSurfaceVariantLight = Color(0xFF444748) +val whiteOutlineLight = Color(0xFF747878) +val whiteOutlineVariantLight = Color(0xFFC4C7C8) +val whiteScrimLight = Color(0xFF000000) +val whiteInverseSurfaceLight = Color(0xFF313030) +val whiteInverseOnSurfaceLight = Color(0xFFF4F0EF) +val whiteInversePrimaryLight = Color(0xFFC6C6C7) +val whiteSurfaceDimLight = Color(0xFFDDD9D9) +val whiteSurfaceBrightLight = Color(0xFFFCF8F8) +val whiteSurfaceContainerLowestLight = Color(0xFFFFFFFF) +val whiteSurfaceContainerLowLight = Color(0xFFF6F3F2) +val whiteSurfaceContainerLight = Color(0xFFF1EDEC) +val whiteSurfaceContainerHighLight = Color(0xFFEBE7E7) +val whiteSurfaceContainerHighestLight = Color(0xFFE5E2E1) + +val whitePrimaryDark = Color(0xFFFFFFFF) +val whiteOnPrimaryDark = Color(0xFF2F3131) +val whitePrimaryContainerDark = Color(0xFFE2E2E2) +val whiteOnPrimaryContainerDark = Color(0xFF636565) +val whiteSecondaryDark = Color(0xFFC8C6C6) +val whiteOnSecondaryDark = Color(0xFF303030) +val whiteSecondaryContainerDark = Color(0xFF494949) +val whiteOnSecondaryContainerDark = Color(0xFFB9B8B8) +val whiteTertiaryDark = Color(0xFFFFFFFF) +val whiteOnTertiaryDark = Color(0xFF2F3131) +val whiteTertiaryContainerDark = Color(0xFFE2E2E2) +val whiteOnTertiaryContainerDark = Color(0xFF636565) +val whiteErrorDark = Color(0xFFFFB4AB) +val whiteOnErrorDark = Color(0xFF690005) +val whiteErrorContainerDark = Color(0xFF93000A) +val whiteOnErrorContainerDark = Color(0xFFFFDAD6) +val whiteBackgroundDark = Color(0xFF141313) +val whiteOnBackgroundDark = Color(0xFFE5E2E1) +val whiteSurfaceDark = Color(0xFF141313) +val whiteOnSurfaceDark = Color(0xFFE5E2E1) +val whiteSurfaceVariantDark = Color(0xFF444748) +val whiteOnSurfaceVariantDark = Color(0xFFC4C7C8) +val whiteOutlineDark = Color(0xFF8E9192) +val whiteOutlineVariantDark = Color(0xFF444748) +val whiteScrimDark = Color(0xFF000000) +val whiteInverseSurfaceDark = Color(0xFFE5E2E1) +val whiteInverseOnSurfaceDark = Color(0xFF313030) +val whiteInversePrimaryDark = Color(0xFF5D5F5F) +val whiteSurfaceDimDark = Color(0xFF141313) +val whiteSurfaceBrightDark = Color(0xFF3A3939) +val whiteSurfaceContainerLowestDark = Color(0xFF0E0E0E) +val whiteSurfaceContainerLowDark = Color(0xFF1C1B1B) +val whiteSurfaceContainerDark = Color(0xFF201F1F) +val whiteSurfaceContainerHighDark = Color(0xFF2A2A2A) +val whiteSurfaceContainerHighestDark = Color(0xFF353434) diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.kt index f61bf532..354bb3e7 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.kt @@ -1,22 +1,23 @@ package com.daniebeler.pfpixelix.ui.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.daniebeler.pfpixelix.di.LocalAppComponent +import com.daniebeler.pfpixelix.domain.model.AppAccentColor import com.daniebeler.pfpixelix.domain.model.AppThemeMode.DARK import com.daniebeler.pfpixelix.domain.model.AppThemeMode.FOLLOW_SYSTEM import com.daniebeler.pfpixelix.domain.model.AppThemeMode.LIGHT -import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.LocalKmpContext fun ColorScheme.toAmoled(): ColorScheme { @@ -126,6 +127,237 @@ private val darkScheme = darkColorScheme( surfaceContainerHighest = surfaceContainerHighestDark, ) + +private val blueLightScheme = lightColorScheme( + primary = bluePrimaryLight, + onPrimary = blueOnPrimaryLight, + primaryContainer = bluePrimaryContainerLight, + onPrimaryContainer = blueOnPrimaryContainerLight, + secondary = blueSecondaryLight, + onSecondary = blueOnSecondaryLight, + secondaryContainer = blueSecondaryContainerLight, + onSecondaryContainer = blueOnSecondaryContainerLight, + tertiary = blueTertiaryLight, + onTertiary = blueOnTertiaryLight, + tertiaryContainer = blueTertiaryContainerLight, + onTertiaryContainer = blueOnTertiaryContainerLight, + error = blueErrorLight, + onError = blueOnErrorLight, + errorContainer = blueErrorContainerLight, + onErrorContainer = blueOnErrorContainerLight, + background = blueBackgroundLight, + onBackground = blueOnBackgroundLight, + surface = blueSurfaceLight, + onSurface = blueOnSurfaceLight, + surfaceVariant = blueSurfaceVariantLight, + onSurfaceVariant = blueOnSurfaceVariantLight, + outline = blueOutlineLight, + outlineVariant = blueOutlineVariantLight, + scrim = blueScrimLight, + inverseSurface = blueInverseSurfaceLight, + inverseOnSurface = blueInverseOnSurfaceLight, + inversePrimary = blueInversePrimaryLight, + surfaceDim = blueSurfaceDimLight, + surfaceBright = blueSurfaceBrightLight, + surfaceContainerLowest = blueSurfaceContainerLowestLight, + surfaceContainerLow = blueSurfaceContainerLowLight, + surfaceContainer = blueSurfaceContainerLight, + surfaceContainerHigh = blueSurfaceContainerHighLight, + surfaceContainerHighest = blueSurfaceContainerHighestLight, +) + +private val blueDarkScheme = darkColorScheme( + primary = bluePrimaryDark, + onPrimary = blueOnPrimaryDark, + primaryContainer = bluePrimaryContainerDark, + onPrimaryContainer = blueOnPrimaryContainerDark, + secondary = blueSecondaryDark, + onSecondary = blueOnSecondaryDark, + secondaryContainer = blueSecondaryContainerDark, + onSecondaryContainer = blueOnSecondaryContainerDark, + tertiary = blueTertiaryDark, + onTertiary = blueOnTertiaryDark, + tertiaryContainer = blueTertiaryContainerDark, + onTertiaryContainer = blueOnTertiaryContainerDark, + error = blueErrorDark, + onError = blueOnErrorDark, + errorContainer = blueErrorContainerDark, + onErrorContainer = blueOnErrorContainerDark, + background = blueBackgroundDark, + onBackground = blueOnBackgroundDark, + surface = blueSurfaceDark, + onSurface = blueOnSurfaceDark, + surfaceVariant = blueSurfaceVariantDark, + onSurfaceVariant = blueOnSurfaceVariantDark, + outline = blueOutlineDark, + outlineVariant = blueOutlineVariantDark, + scrim = blueScrimDark, + inverseSurface = blueInverseSurfaceDark, + inverseOnSurface = blueInverseOnSurfaceDark, + inversePrimary = blueInversePrimaryDark, + surfaceDim = blueSurfaceDimDark, + surfaceBright = blueSurfaceBrightDark, + surfaceContainerLowest = blueSurfaceContainerLowestDark, + surfaceContainerLow = blueSurfaceContainerLowDark, + surfaceContainer = blueSurfaceContainerDark, + surfaceContainerHigh = blueSurfaceContainerHighDark, + surfaceContainerHighest = blueSurfaceContainerHighestDark, +) + +private val redLightScheme = lightColorScheme( + primary = redPrimaryLight, + onPrimary = redOnPrimaryLight, + primaryContainer = redPrimaryContainerLight, + onPrimaryContainer = redOnPrimaryContainerLight, + secondary = redSecondaryLight, + onSecondary = redOnSecondaryLight, + secondaryContainer = redSecondaryContainerLight, + onSecondaryContainer = redOnSecondaryContainerLight, + tertiary = redTertiaryLight, + onTertiary = redOnTertiaryLight, + tertiaryContainer = redTertiaryContainerLight, + onTertiaryContainer = redOnTertiaryContainerLight, + error = redErrorLight, + onError = redOnErrorLight, + errorContainer = redErrorContainerLight, + onErrorContainer = redOnErrorContainerLight, + background = redBackgroundLight, + onBackground = redOnBackgroundLight, + surface = redSurfaceLight, + onSurface = redOnSurfaceLight, + surfaceVariant = redSurfaceVariantLight, + onSurfaceVariant = redOnSurfaceVariantLight, + outline = redOutlineLight, + outlineVariant = redOutlineVariantLight, + scrim = redScrimLight, + inverseSurface = redInverseSurfaceLight, + inverseOnSurface = redInverseOnSurfaceLight, + inversePrimary = redInversePrimaryLight, + surfaceDim = redSurfaceDimLight, + surfaceBright = redSurfaceBrightLight, + surfaceContainerLowest = redSurfaceContainerLowestLight, + surfaceContainerLow = redSurfaceContainerLowLight, + surfaceContainer = redSurfaceContainerLight, + surfaceContainerHigh = redSurfaceContainerHighLight, + surfaceContainerHighest = redSurfaceContainerHighestLight, +) + +private val redDarkScheme = darkColorScheme( + primary = redPrimaryDark, + onPrimary = redOnPrimaryDark, + primaryContainer = redPrimaryContainerDark, + onPrimaryContainer = redOnPrimaryContainerDark, + secondary = redSecondaryDark, + onSecondary = redOnSecondaryDark, + secondaryContainer = redSecondaryContainerDark, + onSecondaryContainer = redOnSecondaryContainerDark, + tertiary = redTertiaryDark, + onTertiary = redOnTertiaryDark, + tertiaryContainer = redTertiaryContainerDark, + onTertiaryContainer = redOnTertiaryContainerDark, + error = redErrorDark, + onError = redOnErrorDark, + errorContainer = redErrorContainerDark, + onErrorContainer = redOnErrorContainerDark, + background = redBackgroundDark, + onBackground = redOnBackgroundDark, + surface = redSurfaceDark, + onSurface = redOnSurfaceDark, + surfaceVariant = redSurfaceVariantDark, + onSurfaceVariant = redOnSurfaceVariantDark, + outline = redOutlineDark, + outlineVariant = redOutlineVariantDark, + scrim = redScrimDark, + inverseSurface = redInverseSurfaceDark, + inverseOnSurface = redInverseOnSurfaceDark, + inversePrimary = redInversePrimaryDark, + surfaceDim = redSurfaceDimDark, + surfaceBright = redSurfaceBrightDark, + surfaceContainerLowest = redSurfaceContainerLowestDark, + surfaceContainerLow = redSurfaceContainerLowDark, + surfaceContainer = redSurfaceContainerDark, + surfaceContainerHigh = redSurfaceContainerHighDark, + surfaceContainerHighest = redSurfaceContainerHighestDark, +) + +private val whiteLightScheme = lightColorScheme( + primary = whitePrimaryLight, + onPrimary = whiteOnPrimaryLight, + primaryContainer = whitePrimaryContainerLight, + onPrimaryContainer = whiteOnPrimaryContainerLight, + secondary = whiteSecondaryLight, + onSecondary = whiteOnSecondaryLight, + secondaryContainer = whiteSecondaryContainerLight, + onSecondaryContainer = whiteOnSecondaryContainerLight, + tertiary = whiteTertiaryLight, + onTertiary = whiteOnTertiaryLight, + tertiaryContainer = whiteTertiaryContainerLight, + onTertiaryContainer = whiteOnTertiaryContainerLight, + error = whiteErrorLight, + onError = whiteOnErrorLight, + errorContainer = whiteErrorContainerLight, + onErrorContainer = whiteOnErrorContainerLight, + background = whiteBackgroundLight, + onBackground = whiteOnBackgroundLight, + surface = whiteSurfaceLight, + onSurface = whiteOnSurfaceLight, + surfaceVariant = whiteSurfaceVariantLight, + onSurfaceVariant = whiteOnSurfaceVariantLight, + outline = whiteOutlineLight, + outlineVariant = whiteOutlineVariantLight, + scrim = whiteScrimLight, + inverseSurface = whiteInverseSurfaceLight, + inverseOnSurface = whiteInverseOnSurfaceLight, + inversePrimary = whiteInversePrimaryLight, + surfaceDim = whiteSurfaceDimLight, + surfaceBright = whiteSurfaceBrightLight, + surfaceContainerLowest = whiteSurfaceContainerLowestLight, + surfaceContainerLow = whiteSurfaceContainerLowLight, + surfaceContainer = whiteSurfaceContainerLight, + surfaceContainerHigh = whiteSurfaceContainerHighLight, + surfaceContainerHighest = whiteSurfaceContainerHighestLight, +) + +private val whiteDarkScheme = darkColorScheme( + primary = whitePrimaryDark, + onPrimary = whiteOnPrimaryDark, + primaryContainer = whitePrimaryContainerDark, + onPrimaryContainer = whiteOnPrimaryContainerDark, + secondary = whiteSecondaryDark, + onSecondary = whiteOnSecondaryDark, + secondaryContainer = whiteSecondaryContainerDark, + onSecondaryContainer = whiteOnSecondaryContainerDark, + tertiary = whiteTertiaryDark, + onTertiary = whiteOnTertiaryDark, + tertiaryContainer = whiteTertiaryContainerDark, + onTertiaryContainer = whiteOnTertiaryContainerDark, + error = whiteErrorDark, + onError = whiteOnErrorDark, + errorContainer = whiteErrorContainerDark, + onErrorContainer = whiteOnErrorContainerDark, + background = whiteBackgroundDark, + onBackground = whiteOnBackgroundDark, + surface = whiteSurfaceDark, + onSurface = whiteOnSurfaceDark, + surfaceVariant = whiteSurfaceVariantDark, + onSurfaceVariant = whiteOnSurfaceVariantDark, + outline = whiteOutlineDark, + outlineVariant = whiteOutlineVariantDark, + scrim = whiteScrimDark, + inverseSurface = whiteInverseSurfaceDark, + inverseOnSurface = whiteInverseOnSurfaceDark, + inversePrimary = whiteInversePrimaryDark, + surfaceDim = whiteSurfaceDimDark, + surfaceBright = whiteSurfaceBrightDark, + surfaceContainerLowest = whiteSurfaceContainerLowestDark, + surfaceContainerLow = whiteSurfaceContainerLowDark, + surfaceContainer = whiteSurfaceContainerDark, + surfaceContainerHigh = whiteSurfaceContainerHighDark, + surfaceContainerHighest = whiteSurfaceContainerHighestDark, +) + + + @Composable fun PixelixTheme( dynamicColor: Boolean = true, @@ -134,33 +366,46 @@ fun PixelixTheme( val prefs = LocalAppComponent.current.preferences val theme by prefs.appThemeModeFlow.collectAsState(prefs.appThemeMode) - LaunchedEffect(theme) { applySystemNightMode(theme) } - var nightModeValue = theme if (nightModeValue == FOLLOW_SYSTEM) { nightModeValue = if (isSystemInDarkTheme()) DARK else LIGHT } - ChangeSystemBarColors(nightModeValue) + LaunchedEffect(nightModeValue) { applySystemNightMode(nightModeValue != LIGHT) } - val context = LocalKmpContext.current - val colorScheme = remember(nightModeValue, dynamicColor, lightScheme, darkScheme) { - context.generateColorScheme(nightModeValue, dynamicColor, lightScheme, darkScheme) + val accentColor by prefs.accentColorFlow.collectAsState(prefs.accentColor) + + val darkColorScheme = when (accentColor) { + AppAccentColor.GREEN -> darkScheme + AppAccentColor.RED -> redDarkScheme + AppAccentColor.BLUE -> blueDarkScheme + AppAccentColor.White -> whiteDarkScheme + else -> darkScheme + } + + val lightColorScheme = when (accentColor) { + AppAccentColor.GREEN -> lightScheme + AppAccentColor.RED -> redLightScheme + AppAccentColor.BLUE -> blueLightScheme + AppAccentColor.White -> whiteLightScheme + else -> darkScheme } + val colorScheme = generateColorScheme(nightModeValue, dynamicColor, lightColorScheme, darkColorScheme) + MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = { + Surface(modifier = Modifier.fillMaxSize(), content = content) + } ) } -expect fun applySystemNightMode(mode: Int) +expect fun applySystemNightMode(isDark: Boolean) @Composable -expect fun ChangeSystemBarColors(mode: Int) - -expect fun KmpContext.generateColorScheme( +expect fun generateColorScheme( nightModeValue: Int, dynamicColor: Boolean, lightScheme: ColorScheme, diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Destinations.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Destinations.kt deleted file mode 100644 index 4215a97a..00000000 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Destinations.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.daniebeler.pfpixelix.utils - -import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.StringResource -import pixelix.app.generated.resources.Res -import pixelix.app.generated.resources.add_circle -import pixelix.app.generated.resources.add_circle_outline -import pixelix.app.generated.resources.alerts -import pixelix.app.generated.resources.bookmark_outline -import pixelix.app.generated.resources.home -import pixelix.app.generated.resources.house -import pixelix.app.generated.resources.house_fill -import pixelix.app.generated.resources.notifications -import pixelix.app.generated.resources.notifications_outline -import pixelix.app.generated.resources.profile -import pixelix.app.generated.resources.search -import pixelix.app.generated.resources.search_outline - -sealed class Destinations( - val route: String, - val icon: DrawableResource = Res.drawable.bookmark_outline, - val activeIcon: DrawableResource = Res.drawable.bookmark_outline, - val label: StringResource = Res.string.home -) { - data object FirstLogin : Destinations( - route = "first_login_screen" - ) - - data object NewLogin : Destinations( - route = "new_login_screen" - ) - - data object HomeScreen : Destinations( - route = "home_screen", - icon = Res.drawable.house, - activeIcon = Res.drawable.house_fill, - label = Res.string.home - ) - - data object NotificationsScreen : Destinations( - route = "notifications_screen", - icon = Res.drawable.notifications_outline, - activeIcon = Res.drawable.notifications, - label = Res.string.alerts - ) - - data object OwnProfile : Destinations( - route = "own_profile_screen", - icon = Res.drawable.bookmark_outline, - activeIcon = Res.drawable.bookmark_outline, - label = Res.string.profile - ) - - data object NewPost : Destinations( - route = "new_post_screen", icon = Res.drawable.add_circle_outline, - activeIcon = Res.drawable.add_circle - ) - - data object Search : Destinations( - route = "search_screen/{initialPage}", - icon = Res.drawable.search_outline, - activeIcon = Res.drawable.search, - label = Res.string.search - ) - - data object Profile : Destinations( - route = "profile_screen/{userid}" - ) - - data object ProfileByUsername : Destinations( - route = "profile_screen/byUsername/{username}" - ) - - data object EditProfile : Destinations( - route = "edit_profile_screen" - ) - - data object IconSelection : Destinations( - route = "icon_selection_screen" - ) - - data object MutedAccounts : Destinations( - route = "muted_accounts_screen" - ) - - data object BlockedAccounts : Destinations( - route = "blocked_accounts_screen" - ) - - data object LikedPosts : Destinations( - route = "liked_posts_screen" - ) - - data object BookmarkedPosts : Destinations( - route = "bookmarked_posts_screen" - ) - - data object FollowedHashtags : Destinations( - route = "followed_hashtags_screen" - ) - - data object AboutInstance : Destinations( - route = "about_instance_screen" - ) - - data object AboutPixelix : Destinations( - route = "about_pixelix_screen" - ) - - data object EditPost : Destinations( - route = "edit_post_screen/{postId}" - ) - - data object Hashtag : Destinations( - route = "hashtag_timeline_screen/{hashtag}" - ) - - data object SinglePost : Destinations( - route = "single_post_screen/{postid}" - ) - - data object Collection : Destinations( - route = "collection_screen/{collectionid}" - ) - - data object Followers : Destinations( - route = "followers_screen/{page}/{userid}" - ) - - - - data object Conversation : Destinations( - route = "conversations" - ) - - data object Chat : Destinations( - route = "chat/{userid}" - ) - - data object Mention : Destinations( - route = "mention/{mentionid}" - ) -} \ 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 04fa414a..f6a4e070 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt @@ -1,9 +1,7 @@ package com.daniebeler.pfpixelix.utils -import androidx.compose.runtime.staticCompositionLocalOf import coil3.PlatformContext import io.github.vinceglb.filekit.core.PlatformFile -import okio.Path expect abstract class KmpUri { abstract override fun toString(): String @@ -14,8 +12,5 @@ expect fun String.toKmpUri(): KmpUri expect fun PlatformFile.toKmpUri(): KmpUri expect abstract class KmpContext -val LocalKmpContext = staticCompositionLocalOf { error("no KmpContext") } expect val KmpContext.coilContext: PlatformContext -expect val KmpContext.dataStoreDir: Path -expect val KmpContext.imageCacheDir: Path diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Navigate.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Navigate.kt deleted file mode 100644 index 5c491c67..00000000 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/Navigate.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.daniebeler.pfpixelix.utils - -import androidx.navigation.NavController - -object Navigate { - private var currentBottomBarRoute: String? = null - private var restoreStateRoutes: List = listOf("home_screen") - - fun changeAccount() { - restoreStateRoutes = emptyList() - } - - fun navigate(route: String, navController: NavController, singleTop: Boolean = true) { - if (navController.currentDestination!!.route == route) { - return - } - val alreadySaved = restoreStateRoutes.indexOf(route) != -1 - if (!alreadySaved) { - restoreStateRoutes = restoreStateRoutes + route - } - navController.navigate(route) { - launchSingleTop = singleTop - restoreState = alreadySaved - } - } - - fun navigateWithPopUp(newRoute: String, navController: NavController) { - if (navController.currentDestination!!.route == newRoute) { - return - } - val alreadySaved = restoreStateRoutes.indexOf(newRoute) != -1 - if (!alreadySaved) { - restoreStateRoutes = restoreStateRoutes + newRoute - } - if (newRoute == currentBottomBarRoute) { - navController.navigate(newRoute) { - popUpTo(currentBottomBarRoute!!) - launchSingleTop = true - } - } else { - navController.navigate(newRoute) { - popUpTo(0) { - saveState = true - } - launchSingleTop = true - restoreState = alreadySaved - } - } - currentBottomBarRoute = newRoute - } - - fun navigateAndDeleteBackStack(route: String, navController: NavController) { - navController.navigate(route) { - popUpTo(0) { - inclusive = true - } - - launchSingleTop = true - } - } -} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/TimeAgo.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/TimeAgo.kt index 42916f6b..b1eb6abf 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/TimeAgo.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/TimeAgo.kt @@ -2,12 +2,23 @@ package com.daniebeler.pfpixelix.utils import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import org.jetbrains.compose.resources.getPluralString +import org.jetbrains.compose.resources.getString +import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.ago +import pixelix.app.generated.resources.second +import pixelix.app.generated.resources.minute +import pixelix.app.generated.resources.hour +import pixelix.app.generated.resources.day +import pixelix.app.generated.resources.week +import pixelix.app.generated.resources.month +import pixelix.app.generated.resources.year object TimeAgo { - fun convertTimeToText(dataDate: String): String { + suspend fun convertTimeToText(dataDate: String): String { var convTime: String = "" - val suffix = "ago" + val suffix = getString(Res.string.ago) try { val pasTime: Instant = Instant.parse(dataDate) val nowTime: Instant = Clock.System.now() @@ -18,21 +29,38 @@ object TimeAgo { val hour: Long = dateDiff.inWholeHours val day: Long = dateDiff.inWholeDays if (second < 60) { - convTime = "$second seconds $suffix" + convTime = + "$second ${getPluralString(Res.plurals.second, second.toInt(), 1)} $suffix" } else if (minute < 60) { - convTime = "$minute minutes $suffix" + convTime = + "$minute ${getPluralString(Res.plurals.minute, minute.toInt(), 1)} $suffix" } else if (hour < 24) { - convTime = "$hour hours $suffix" + convTime = "$hour ${getPluralString(Res.plurals.hour, hour.toInt(), 1)} $suffix" } else if (day >= 7) { - convTime = if (day > 360) { - (day / 360).toString() + " years " + suffix + if (day > 360) { + convTime = "${(day / 360)} ${ + getPluralString( + Res.plurals.year, + (day / 360).toInt() + ) + } $suffix" } else if (day > 30) { - (day / 30).toString() + " months " + suffix + convTime = "${(day / 30)} ${ + getPluralString( + Res.plurals.month, + (day / 30).toInt() + ) + } $suffix" } else { - (day / 7).toString() + " week " + suffix + convTime = "${(day / 7)} ${ + getPluralString( + Res.plurals.week, + (day / 7).toInt() + ) + } $suffix" } } else if (day < 7) { - convTime = "$day days $suffix" + convTime = "$day ${getPluralString(Res.plurals.day, day.toInt())} $suffix" } } catch (e: IllegalArgumentException) { e.printStackTrace() diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.kt index 7171e692..588d321d 100644 --- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.kt +++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.kt @@ -10,6 +10,7 @@ expect class VideoPlayer( ) { var progress: ((current: Long, duration: Long) -> Unit)? var hasAudio: ((Boolean) -> Unit)? + var isVideoPlaying: ((Boolean) -> Unit)? @Composable fun view(modifier: Modifier) diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/App.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/App.ios.kt index c9a5ed80..c591f76a 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/App.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/App.ios.kt @@ -6,18 +6,13 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @OptIn(ExperimentalComposeUiApi::class) -@Composable -actual fun EdgeToEdgeDialog( - onDismissRequest: () -> Unit, - properties: DialogProperties, - content: @Composable () -> Unit -) = Dialog( - onDismissRequest = onDismissRequest, - properties = DialogProperties( - dismissOnBackPress = properties.dismissOnBackPress, - dismissOnClickOutside = properties.dismissOnClickOutside, - usePlatformDefaultWidth = properties.usePlatformDefaultWidth, - usePlatformInsets = false - ), - content = content +actual fun EdgeToEdgeDialogProperties( + dismissOnBackPress: Boolean, + dismissOnClickOutside: Boolean, + usePlatformDefaultWidth: Boolean +): DialogProperties = DialogProperties( + dismissOnBackPress = dismissOnBackPress, + dismissOnClickOutside = dismissOnClickOutside, + usePlatformDefaultWidth = usePlatformDefaultWidth, + usePlatformInsets = false ) diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt index 2e99e206..9b796e37 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/AppViewController.kt @@ -2,13 +2,14 @@ package com.daniebeler.pfpixelix import androidx.compose.foundation.ComposeFoundationFlags import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.ExperimentalComposeUiApi 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.LocalKmpContext import com.daniebeler.pfpixelix.utils.configureLogger import platform.UIKit.UIViewController @@ -16,17 +17,19 @@ class IosUrlCallback { var onRedirect: (String) -> Unit = {} } -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) fun AppViewController(urlCallback: IosUrlCallback): UIViewController { //https://youtrack.jetbrains.com/issue/CMP-7623 iOS - Gesture handling is incorrect in 1.8.0-alpha03 ComposeFoundationFlags.DragGesturePickUpEnabled = false var viewController: UIViewController? = null - val context = object : KmpContext() { - override val viewController: UIViewController - get() = viewController!! - } - val appComponent = AppComponent.Companion.create(context) + val appComponent = AppComponent.Companion.create( + object : KmpContext() { + override val viewController get() = viewController!! + }, + IosFileService(), + IosAppIconManager() + ) configureLogger() @@ -39,12 +42,12 @@ fun AppViewController(urlCallback: IosUrlCallback): UIViewController { } val finishApp = {} - viewController = ComposeUIViewController { - CompositionLocalProvider( - LocalKmpContext provides context - ) { - App(appComponent, finishApp) + viewController = ComposeUIViewController( + configure = { + parallelRendering = true } + ) { + App(appComponent, finishApp) } return viewController 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 new file mode 100644 index 00000000..6b7fda13 --- /dev/null +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/IosFileService.kt @@ -0,0 +1,175 @@ +package com.daniebeler.pfpixelix.domain.service.file + +import com.daniebeler.pfpixelix.utils.KmpUri +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.get +import kotlinx.cinterop.refTo +import kotlinx.cinterop.usePinned +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext +import okio.Path +import okio.Path.Companion.toPath +import platform.CoreFoundation.CFDataGetBytePtr +import platform.CoreFoundation.CFDataGetLength +import platform.CoreFoundation.CFDictionaryAddValue +import platform.CoreFoundation.CFDictionaryCreateMutable +import platform.CoreFoundation.CFRelease +import platform.CoreFoundation.CFStringRef +import platform.CoreFoundation.CFURLRef +import platform.CoreGraphics.CGDataProviderCopyData +import platform.CoreGraphics.CGImageGetDataProvider +import platform.CoreServices.UTTypeCopyPreferredTagWithClass +import platform.CoreServices.UTTypeCreatePreferredIdentifierForTag +import platform.CoreServices.kUTTagClassFilenameExtension +import platform.CoreServices.kUTTagClassMIMEType +import platform.Foundation.CFBridgingRelease +import platform.Foundation.CFBridgingRetain +import platform.Foundation.NSData +import platform.Foundation.NSDictionary +import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSFileSize +import platform.Foundation.NSNumber +import platform.Foundation.NSString +import platform.Foundation.NSUserDomainMask +import platform.Foundation.dataWithContentsOfURL +import platform.Foundation.fileSize +import platform.ImageIO.CGImageSourceCreateThumbnailAtIndex +import platform.ImageIO.CGImageSourceCreateWithURL +import platform.ImageIO.kCGImageSourceCreateThumbnailFromImageAlways +import platform.ImageIO.kCGImageSourceCreateThumbnailWithTransform +import platform.ImageIO.kCGImageSourceThumbnailMaxPixelSize +import platform.posix.memcpy + +@OptIn(ExperimentalForeignApi::class) +class IosFileService : FileService { + private fun appDocDir() = NSFileManager.defaultManager.URLForDirectory( + directory = NSDocumentDirectory, + inDomain = NSUserDomainMask, + appropriateForURL = null, + create = false, + error = null, + )!!.path!!.toPath() + + override val dataStoreDir: Path = appDocDir().resolve("dataStore") + override val imageCacheDir: Path = appDocDir().resolve("imageCache") + + + override fun getFile(uri: KmpUri): PlatformFile? { + return IosFile(uri).takeIf { it.isExist() } + } + + override fun downloadFile(name: String?, url: String) { + } + + override fun getCacheSizeInBytes(): Long { + val fm = NSFileManager.defaultManager() + val files = fm.subpathsOfDirectoryAtPath(imageCacheDir.toString(), null).orEmpty() + var result = 0uL + files.map { file -> + val dict = fm.fileAttributesAtPath( + imageCacheDir.resolve(file.toString()).toString(), + true + ) as NSDictionary + result += dict.fileSize() + } + return result.toLong() + } + + override fun cleanCache() { + val fm = NSFileManager.defaultManager() + fm.removeItemAtPath(imageCacheDir.toString(), null) + } +} + +@OptIn(ExperimentalForeignApi::class) +private class IosFile( + private val uri: KmpUri +) : PlatformFile { + override fun isExist(): Boolean = + getName() != "IosFile:unknown" + + override fun getName(): String { + return uri.url.lastPathComponent() ?: "IosFile:unknown" + } + + override fun getSize(): Long { + val path = uri.url.path ?: return 0L + val fm = NSFileManager.defaultManager + val attr = fm.attributesOfItemAtPath(path, null) ?: return 0L + return attr.getValue(NSFileSize) as Long + } + + override fun getMimeType(): String { + val fileExtension = uri.url.pathExtension() + @Suppress("UNCHECKED_CAST", "CAST_NEVER_SUCCEEDS") + val fileExtensionRef = CFBridgingRetain(fileExtension as NSString) as CFStringRef + val uti = UTTypeCreatePreferredIdentifierForTag( + kUTTagClassFilenameExtension, + fileExtensionRef, + null + ) + CFRelease(fileExtensionRef) + val mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType) + CFRelease(uti) + return CFBridgingRelease(mimeType) as String + } + + override suspend fun readBytes(): ByteArray = withContext(Dispatchers.IO) { + val data = NSData.dataWithContentsOfURL(uri.url)!! + ByteArray(data.length.toInt()).apply { + data.usePinned { + memcpy(refTo(0), data.bytes, data.length) + } + } + } + + override suspend fun getThumbnail(): ByteArray? = withContext(Dispatchers.IO) { + @Suppress("UNCHECKED_CAST") + val urlRef = CFBridgingRetain(uri.url) as CFURLRef + val imageSource = CGImageSourceCreateWithURL(urlRef, null)!! + val thumbnailOptions = CFDictionaryCreateMutable( + null, + 3, + null, + null + ).apply { + CFDictionaryAddValue( + this, + kCGImageSourceCreateThumbnailWithTransform, + CFBridgingRetain(NSNumber(bool = true)) + ) + CFDictionaryAddValue( + this, + kCGImageSourceCreateThumbnailFromImageAlways, + CFBridgingRetain(NSNumber(bool = true)) + ) + CFDictionaryAddValue( + this, + kCGImageSourceThumbnailMaxPixelSize, + CFBridgingRetain(NSNumber(512)) + ) + } + + val thumbnailSource = CGImageSourceCreateThumbnailAtIndex( + imageSource, + 0u, + thumbnailOptions + ) + + val data = CGDataProviderCopyData(CGImageGetDataProvider(thumbnailSource)) + val bytePointer = CFDataGetBytePtr(data)!! + val length = CFDataGetLength(data) + + val byteArray = ByteArray(length.toInt()) { index -> + bytePointer[index].toByte() + } + + CFRelease(urlRef) + CFRelease(data) + CFRelease(thumbnailSource) + + byteArray + } +} \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/IosAppIconManager.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/IosAppIconManager.kt new file mode 100644 index 00000000..f2241a67 --- /dev/null +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/IosAppIconManager.kt @@ -0,0 +1,48 @@ +package com.daniebeler.pfpixelix.domain.service.icon + +import org.jetbrains.compose.resources.DrawableResource +import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.app_icon_00 +import pixelix.app.generated.resources.app_icon_01 +import pixelix.app.generated.resources.app_icon_02 +import pixelix.app.generated.resources.app_icon_03 +import pixelix.app.generated.resources.app_icon_05 +import pixelix.app.generated.resources.app_icon_06 +import pixelix.app.generated.resources.app_icon_07 +import pixelix.app.generated.resources.app_icon_08 +import pixelix.app.generated.resources.app_icon_09 +import platform.UIKit.UIApplication +import platform.UIKit.alternateIconName +import platform.UIKit.setAlternateIconName + +class IosAppIconManager : AppIconManager { + private val iconIds = mapOf( + Res.drawable.app_icon_00 to "AppIcon_02", + Res.drawable.app_icon_01 to "AppIcon_01", + Res.drawable.app_icon_02 to "AppIcon", + Res.drawable.app_icon_03 to "AppIcon_03", + Res.drawable.app_icon_05 to "AppIcon_04", + Res.drawable.app_icon_06 to "AppIcon_05", + Res.drawable.app_icon_07 to "AppIcon_06", + Res.drawable.app_icon_08 to "AppIcon_07", + Res.drawable.app_icon_09 to "AppIcon_08", + ) + + override fun getCurrentIcon(): DrawableResource { + val currentId = UIApplication.sharedApplication.alternateIconName + for ((res, id) in iconIds.entries) { + if (currentId == id) { + return res + } + } + return Res.drawable.app_icon_02 + } + + override fun setCustomIcon(icon: DrawableResource) { + if (icon == Res.drawable.app_icon_02) { + UIApplication.sharedApplication.setAlternateIconName(null, null) + } else { + UIApplication.sharedApplication.setAlternateIconName(iconIds[icon]!!, null) + } + } +} \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.ios.kt index f6adfdc9..26b28d6f 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.ios.kt @@ -1,232 +1,79 @@ package com.daniebeler.pfpixelix.domain.service.platform +import co.touchlab.kermit.Logger import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences import com.daniebeler.pfpixelix.utils.KmpContext -import com.daniebeler.pfpixelix.utils.KmpUri -import com.daniebeler.pfpixelix.utils.imageCacheDir 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 kotlinx.cinterop.useContents import me.tatarka.inject.annotations.Inject -import org.jetbrains.compose.resources.DrawableResource -import pixelix.app.generated.resources.Res -import pixelix.app.generated.resources.app_icon_00 -import pixelix.app.generated.resources.app_icon_01 -import pixelix.app.generated.resources.app_icon_02 -import pixelix.app.generated.resources.app_icon_03 -import pixelix.app.generated.resources.app_icon_05 -import pixelix.app.generated.resources.app_icon_06 -import pixelix.app.generated.resources.app_icon_07 -import pixelix.app.generated.resources.app_icon_08 -import pixelix.app.generated.resources.app_icon_09 -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.CoreGraphics.CGRectMake import platform.Foundation.NSBundle -import platform.Foundation.NSData -import platform.Foundation.NSDictionary -import platform.Foundation.NSFileManager -import platform.Foundation.NSFileSize -import platform.Foundation.NSNumber -import platform.Foundation.NSString import platform.Foundation.NSURL -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.Foundation.NSURL.Companion.URLWithString +import platform.SafariServices.SFSafariViewController import platform.UIKit.UIActivityViewController import platform.UIKit.UIApplication -import platform.UIKit.alternateIconName -import platform.UIKit.setAlternateIconName -import platform.posix.memcpy +import platform.UIKit.UIDevice +import platform.UIKit.UIUserInterfaceIdiomPad +import platform.UIKit.popoverPresentationController @Inject actual class Platform actual constructor( private val context: KmpContext, private val prefs: UserPreferences ) { - actual fun getPlatformFile(uri: KmpUri): PlatformFile? { - val f = IosFile(uri) - return if (f.getName() != "IosFile:unknown") f else null - } - - actual fun getAppIconManager(): AppIconManager { - return IosAppIconManager() - } - actual fun openUrl(url: String) { - UIApplication.sharedApplication.openURL(NSURL(string = url)) - } - - actual fun shareText(text: String) { - val vc = UIActivityViewController(listOf(text), null) - context.viewController.presentViewController(vc, true, null) - } + if (prefs.useInAppBrowser) { + val safariViewController = SFSafariViewController(uRL = NSURL(string = url)) + val self = context.viewController + self.presentViewController( + viewControllerToPresent = safariViewController, + animated = true, + completion = null + ) - actual fun getAppVersion(): String { - return NSBundle.mainBundle.infoDictionary?.get("CFBundleShortVersionString").toString() + } else { + UIApplication.sharedApplication.openURL( + url = URLWithString(url)!!, + options = emptyMap(), + completionHandler = null + ) + } } - actual fun pinWidget() {} - - actual fun downloadImageToGallery(name: String?, url: String) {} - - @OptIn(ExperimentalForeignApi::class) - actual fun getCacheSizeInBytes(): Long { - val fm = NSFileManager.defaultManager() - val cacheDir = context.imageCacheDir - val files = fm.subpathsOfDirectoryAtPath(cacheDir.toString(), null).orEmpty() - var result = 0uL - files.map { file -> - val dict = fm.fileAttributesAtPath( - cacheDir.resolve(file.toString()).toString(), - true - ) as NSDictionary - result += dict.fileSize() + actual fun dismissBrowser() { + if (prefs.useInAppBrowser) { + val self = context.viewController + self.dismissModalViewControllerAnimated(true) } - return result.toLong() } @OptIn(ExperimentalForeignApi::class) - actual fun cleanCache() { - val fm = NSFileManager.defaultManager() - fm.removeItemAtPath(context.imageCacheDir.toString(), null) - } -} - -@OptIn(ExperimentalForeignApi::class) -private class IosFile( - private val uri: KmpUri -) : PlatformFile { - 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() - val fileExtensionRef = CFBridgingRetain(fileExtension as NSString) as CFStringRef - val uti = UTTypeCreatePreferredIdentifierForTag( - kUTTagClassFilenameExtension, - fileExtensionRef, - null + actual fun shareText(text: String) { + val self = context.viewController + val vc = UIActivityViewController( + activityItems = listOf(text), + applicationActivities = 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) + if (isIpad()) { + Logger.d("share on iPad") + vc.popoverPresentationController?.apply { + sourceView = self.view + sourceRect = self.view.center.useContents { CGRectMake(x, y, 0.0, 0.0) } + permittedArrowDirections = 0uL } } + self.presentViewController(vc, true, null) } - override suspend fun getThumbnail(): ByteArray? = withContext(Dispatchers.IO) { - val urlRef = CFBridgingRetain(uri.url) as CFURLRef - val imageSource = CGImageSourceCreateWithURL(urlRef, null)!! - val thumbnailOptions = CFDictionaryCreateMutable( - null, - 3, - null, - null - ).apply { - CFDictionaryAddValue( - this, - kCGImageSourceCreateThumbnailWithTransform, - CFBridgingRetain(NSNumber(bool = true)) - ) - CFDictionaryAddValue( - this, - kCGImageSourceCreateThumbnailFromImageAlways, - CFBridgingRetain(NSNumber(bool = true)) - ) - CFDictionaryAddValue( - this, - kCGImageSourceThumbnailMaxPixelSize, - CFBridgingRetain(NSNumber(512)) - ) - } - - val thumbnailSource = CGImageSourceCreateThumbnailAtIndex( - imageSource, - 0u, - thumbnailOptions - ) - - val data = CGDataProviderCopyData(CGImageGetDataProvider(thumbnailSource)) - val bytePointer = CFDataGetBytePtr(data)!! - val length = CFDataGetLength(data) - - val byteArray = ByteArray(length.toInt()) { index -> - bytePointer[index].toByte() - } - - CFRelease(urlRef) - CFRelease(data) - CFRelease(thumbnailSource) - - byteArray + private fun isIpad(): Boolean { + val device = UIDevice.currentDevice + return device.userInterfaceIdiom == UIUserInterfaceIdiomPad } -} -private class IosAppIconManager : AppIconManager { - private val iconIds = mapOf( - Res.drawable.app_icon_00 to "AppIcon_02", - Res.drawable.app_icon_01 to "AppIcon_01", - Res.drawable.app_icon_02 to "AppIcon", - Res.drawable.app_icon_03 to "AppIcon_03", - Res.drawable.app_icon_05 to "AppIcon_04", - Res.drawable.app_icon_06 to "AppIcon_05", - Res.drawable.app_icon_07 to "AppIcon_06", - Res.drawable.app_icon_08 to "AppIcon_07", - Res.drawable.app_icon_09 to "AppIcon_08", - ) - - override fun getCurrentIcon(): DrawableResource { - val currentId = UIApplication.sharedApplication.alternateIconName - for ((res, id) in iconIds.entries) { - if (currentId == id) { - return res - } - } - return Res.drawable.app_icon_02 + actual fun getAppVersion(): String { + return NSBundle.mainBundle.infoDictionary?.get("CFBundleShortVersionString").toString() } - override fun setCustomIcon(icon: DrawableResource) { - if (icon == Res.drawable.app_icon_02) { - UIApplication.sharedApplication.setAlternateIconName(null, null) - } else { - UIApplication.sharedApplication.setAlternateIconName(iconIds[icon]!!, null) - } - } + actual fun pinWidget() {} } 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 8976de5d..d44c2ceb 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 @@ -2,6 +2,10 @@ package com.daniebeler.pfpixelix.domain.service.platform actual object PlatformFeatures { actual val notificationWidgets = false - actual val inAppBrowser = false + actual val inAppBrowser = true actual val downloadToGallery = false + actual val customAppIcon = true + actual val autoplayVideosPref = false + actual val addCollection = false + actual val customAccentColors = true } \ No newline at end of file diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.ios.kt index 0463fdbe..d9885023 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.ios.kt @@ -2,25 +2,27 @@ package com.daniebeler.pfpixelix.ui.theme import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import com.daniebeler.pfpixelix.domain.model.AppThemeMode -import com.daniebeler.pfpixelix.utils.KmpContext -actual fun applySystemNightMode(mode: Int) {} +actual fun applySystemNightMode(isDark: Boolean) {} @Composable -actual fun ChangeSystemBarColors(mode: Int) {} - -actual fun KmpContext.generateColorScheme( +actual fun generateColorScheme( nightModeValue: Int, dynamicColor: Boolean, lightScheme: ColorScheme, darkScheme: ColorScheme ): ColorScheme { //TODO dynamicColor - return when (nightModeValue) { - AppThemeMode.AMOLED -> darkScheme.toAmoled() - AppThemeMode.DARK -> darkScheme - else -> lightScheme + return remember( + nightModeValue, dynamicColor, lightScheme, darkScheme + ) { + when (nightModeValue) { + AppThemeMode.AMOLED -> darkScheme.toAmoled() + AppThemeMode.DARK -> darkScheme + else -> lightScheme + } } } \ 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 9bef7c96..a17bae93 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,12 +2,7 @@ package com.daniebeler.pfpixelix.utils import coil3.PlatformContext import io.github.vinceglb.filekit.core.PlatformFile -import kotlinx.cinterop.ExperimentalForeignApi -import okio.Path.Companion.toPath -import platform.Foundation.NSDocumentDirectory -import platform.Foundation.NSFileManager import platform.Foundation.NSURL -import platform.Foundation.NSUserDomainMask import platform.UIKit.UIViewController private data class IosUri(override val url: NSURL) : KmpUri() { @@ -27,15 +22,3 @@ actual abstract class KmpContext { abstract val viewController: UIViewController } actual val KmpContext.coilContext get() = PlatformContext.INSTANCE -actual val KmpContext.dataStoreDir get() = appDocDir().resolve("dataStore") -actual val KmpContext.imageCacheDir get() = appDocDir().resolve("imageCache") - -@OptIn(ExperimentalForeignApi::class) -private fun appDocDir() = NSFileManager.defaultManager.URLForDirectory( - directory = NSDocumentDirectory, - inDomain = NSUserDomainMask, - appropriateForURL = null, - create = false, - error = null, -)!!.path!!.toPath() - 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 index 3e3bbdf6..4d667e5b 100644 --- a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.ios.kt +++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.ios.kt @@ -28,6 +28,7 @@ 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) @@ -37,6 +38,7 @@ actual class VideoPlayer actual constructor( ) { 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 @@ -86,7 +88,6 @@ actual class VideoPlayer actual constructor( actual fun prepare(url: String) { release() - val item = AVPlayerItem(NSURL(string = url)) player.replaceCurrentItemWithPlayerItem(item) diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/App.jvm.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/App.jvm.kt new file mode 100644 index 00000000..6ce16a38 --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/App.jvm.kt @@ -0,0 +1,16 @@ +package com.daniebeler.pfpixelix + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.DialogProperties + +@OptIn(ExperimentalComposeUiApi::class) +actual fun EdgeToEdgeDialogProperties( + dismissOnBackPress: Boolean, + dismissOnClickOutside: Boolean, + usePlatformDefaultWidth: Boolean +): DialogProperties = DialogProperties( + dismissOnBackPress = dismissOnBackPress, + dismissOnClickOutside = dismissOnClickOutside, + usePlatformDefaultWidth = usePlatformDefaultWidth, + usePlatformInsets = false +) diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt new file mode 100644 index 00000000..ffc09aca --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/Main.kt @@ -0,0 +1,50 @@ +package com.daniebeler.pfpixelix + +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import coil3.SingletonImageLoader +import com.daniebeler.pfpixelix.di.AppComponent +import com.daniebeler.pfpixelix.di.create +import com.daniebeler.pfpixelix.domain.service.file.DesktopFileService +import com.daniebeler.pfpixelix.domain.service.icon.DesktopAppIconManager +import com.daniebeler.pfpixelix.utils.KmpContext +import com.daniebeler.pfpixelix.utils.configureJavaLogger +import com.daniebeler.pfpixelix.utils.configureLogger +import 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 { + val appComponent = AppComponent.Companion.create( + object : KmpContext() {}, + DesktopFileService(), + DesktopAppIconManager() + ) + + configureJavaLogger() + + SingletonImageLoader.setSafe { + appComponent.provideImageLoader() + } + + Desktop.getDesktop().setOpenURIHandler { url -> + appComponent.systemUrlHandler.onRedirect( + url.uri.toString() + ) + } + + Window( + title = "Pixelix", + state = rememberWindowState(width = 600.dp, height = 1000.dp), + onCloseRequest = ::exitApplication, + ) { + window.minimumSize = Dimension(400, 600) + App(appComponent) { exitApplication() } + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..580bd0ae --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/file/DesktopFileService.kt @@ -0,0 +1,79 @@ +package com.daniebeler.pfpixelix.domain.service.file + +import ca.gosyer.appdirs.AppDirs +import co.touchlab.kermit.Logger +import com.daniebeler.pfpixelix.utils.KmpUri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okio.Path +import okio.Path.Companion.toPath +import java.awt.Image +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.File +import java.nio.file.Files +import javax.imageio.ImageIO + +class DesktopFileService : FileService { + private val appDirs = AppDirs("com.daniebeler.pfpixelix", null) + private fun appDocDir() = appDirs.getUserDataDir().toPath() + + override val dataStoreDir: Path = appDocDir().resolve("dataStore") + override val imageCacheDir: Path = appDocDir().resolve("imageCache") + + override fun getFile(uri: KmpUri): PlatformFile? { + return DesktopFile(uri).takeIf { it.isExist() } + } + + override fun downloadFile(name: String?, url: String) { + } + + override fun getCacheSizeInBytes(): Long { + return imageCacheDir.toFile().walkBottomUp().fold(0L) { acc, file -> acc + file.length() } + } + + override fun cleanCache() { + imageCacheDir.toFile().deleteRecursively() + } +} + +private class DesktopFile( + uri: KmpUri +) : PlatformFile { + private val file = File(uri.uri) + + override fun isExist(): Boolean = file.exists() + override fun getName(): String = file.name + override fun getSize(): Long = file.length() + override fun getMimeType(): String = Files.probeContentType(file.toPath()) + + override suspend fun readBytes(): ByteArray = withContext(Dispatchers.IO) { + file.readBytes() + } + + override suspend fun getThumbnail(): ByteArray? = withContext(Dispatchers.IO) { + val thumbnail = try { + val size = 512 + val originalImage = ImageIO.read(file) + val aspectRatio = originalImage.width.toDouble() / originalImage.height + val (width, height) = if (aspectRatio > 1) { + size to (size / aspectRatio).toInt() + } else { + (size * aspectRatio).toInt() to size + } + val image = originalImage.getScaledInstance(width, height, Image.SCALE_SMOOTH) + val bufferedImage = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) + val graphics = bufferedImage.createGraphics() + graphics.drawImage(image, 0, 0, null) + graphics.dispose() + bufferedImage + } catch (e: Exception) { + Logger.e("Failed to create thumbnail for file: ${file.name}", e) + null + } ?: return@withContext null + + val outputStream = ByteArrayOutputStream() + ImageIO.write(thumbnail, "png", outputStream) + outputStream.toByteArray() + } +} \ No newline at end of file diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/DesktopAppIconManager.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/DesktopAppIconManager.kt new file mode 100644 index 00000000..6c593b40 --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/icon/DesktopAppIconManager.kt @@ -0,0 +1,14 @@ +package com.daniebeler.pfpixelix.domain.service.icon + +import org.jetbrains.compose.resources.DrawableResource +import pixelix.app.generated.resources.Res +import pixelix.app.generated.resources.app_icon_02 + +class DesktopAppIconManager : AppIconManager { + + override fun getCurrentIcon(): DrawableResource { + return Res.drawable.app_icon_02 + } + + override fun setCustomIcon(icon: DrawableResource) {} +} \ No newline at end of file diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.jvm.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.jvm.kt new file mode 100644 index 00000000..f917a235 --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/Platform.jvm.kt @@ -0,0 +1,27 @@ +package com.daniebeler.pfpixelix.domain.service.platform + +import com.daniebeler.pfpixelix.domain.service.preferences.UserPreferences +import com.daniebeler.pfpixelix.utils.KmpContext +import me.tatarka.inject.annotations.Inject +import java.awt.Desktop +import java.net.URI + +@Inject +actual class Platform actual constructor( + private val context: KmpContext, + private val prefs: UserPreferences +) { + actual fun openUrl(url: String) { + Desktop.getDesktop().browse(URI(url)) + } + + actual fun dismissBrowser() {} + + actual fun shareText(text: String) {} + + actual fun getAppVersion(): String { + return "1.0.0" + } + + actual fun pinWidget() {} +} 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 new file mode 100644 index 00000000..a44074ef --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/domain/service/platform/PlatformFeatures.jvm.kt @@ -0,0 +1,11 @@ +package com.daniebeler.pfpixelix.domain.service.platform + +actual object PlatformFeatures { + actual val notificationWidgets = false + actual val inAppBrowser = false + actual val downloadToGallery = false + actual val customAppIcon = false + actual val autoplayVideosPref = false + actual val addCollection = true + actual val customAccentColors = true +} \ No newline at end of file diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.jvm.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.jvm.kt new file mode 100644 index 00000000..d9885023 --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/ui/theme/Theme.jvm.kt @@ -0,0 +1,28 @@ +package com.daniebeler.pfpixelix.ui.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.daniebeler.pfpixelix.domain.model.AppThemeMode + + +actual fun applySystemNightMode(isDark: Boolean) {} + +@Composable +actual fun generateColorScheme( + nightModeValue: Int, + dynamicColor: Boolean, + lightScheme: ColorScheme, + darkScheme: ColorScheme +): ColorScheme { + //TODO dynamicColor + return remember( + nightModeValue, dynamicColor, lightScheme, darkScheme + ) { + when (nightModeValue) { + AppThemeMode.AMOLED -> darkScheme.toAmoled() + AppThemeMode.DARK -> darkScheme + else -> lightScheme + } + } +} \ No newline at end of file diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/BlurHashDecoder.jvm.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/BlurHashDecoder.jvm.kt new file mode 100644 index 00000000..137d0c3e --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/BlurHashDecoder.jvm.kt @@ -0,0 +1,31 @@ +package com.daniebeler.pfpixelix.utils + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.asSkiaBitmap +import org.jetbrains.skia.EncodedImageFormat +import org.jetbrains.skia.Image + +actual fun createBitmap( + pixels: IntArray, + width: Int, + height: Int +) = ImageBitmap(width, height, ImageBitmapConfig.Argb8888).also { bitmap -> + val pixelBytes = ByteArray(pixels.size * 4) { i -> + val pixel = pixels[i / 4] + val color = when (i % 4) { + 0 -> (pixel shr 16 and 0xFF)// Red + 1 -> (pixel shr 8 and 0xFF)// Green + 2 -> (pixel and 0xFF)// Blue + else -> (pixel shr 24 and 0xFF)// Alpha + } + color.toByte() + } + bitmap.asSkiaBitmap().installPixels(pixelBytes) +} + +actual fun ImageBitmap.encodeToPngBytes(quality: Int): ByteArray? { + return Image.makeFromBitmap(this.asSkiaBitmap()) + .encodeToData(EncodedImageFormat.PNG, quality) + ?.bytes +} \ No newline at end of file diff --git a/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/JvmLogger.kt b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/JvmLogger.kt new file mode 100644 index 00000000..7cb490ac --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/JvmLogger.kt @@ -0,0 +1,34 @@ +package com.daniebeler.pfpixelix.utils + +import co.touchlab.kermit.LogWriter +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import org.slf4j.LoggerFactory +import org.slf4j.event.Level + +fun configureJavaLogger(isDebug: Boolean = false) { + configureLogger(isDebug) + Logger.addLogWriter(Slf4jLogWriter) +} + +private object Slf4jLogWriter : LogWriter() { + override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { + val logger = LoggerFactory.getLogger(tag).atLevel(severity.slf4jLevel) + + if (throwable != null) { + logger.setCause(throwable) + } + + logger.log(message) + } + + private val Severity.slf4jLevel: Level + get() = when (this) { + Severity.Verbose -> Level.TRACE + Severity.Debug -> Level.DEBUG + Severity.Info -> Level.INFO + Severity.Warn -> Level.WARN + Severity.Error -> Level.ERROR + Severity.Assert -> Level.ERROR + } +} \ 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 new file mode 100644 index 00000000..c945d542 --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.jvm.kt @@ -0,0 +1,21 @@ +package com.daniebeler.pfpixelix.utils + +import coil3.PlatformContext +import io.github.vinceglb.filekit.core.PlatformFile +import java.net.URI + +private data class DesktopUri(override val uri: URI) : KmpUri() { + override fun toString(): String = uri.toString() +} + +actual abstract class KmpUri { + abstract val uri: URI + actual abstract override fun toString(): String +} +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 abstract class KmpContext +actual val KmpContext.coilContext get() = PlatformContext.INSTANCE 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 new file mode 100644 index 00000000..f46fd721 --- /dev/null +++ b/app/src/jvmMain/kotlin/com/daniebeler/pfpixelix/utils/VideoPlayer.jvm.kt @@ -0,0 +1,101 @@ +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 139046b5..3022f950 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,57 +1,61 @@ [versions] -kotlin = "2.1.10" -ksp = "2.1.10-1.0.29" -agp = "8.8.0" +kotlin = "2.1.20" +ksp = "2.1.20-1.0.31" +agp = "8.9.1" #https://github.com/JetBrains/compose-multiplatform/releases -composeMultiplatform = "1.8.0-alpha03" -lifecycle = "2.9.0-alpha03" -lifecycleMultiplatform = "2.9.0-alpha03" -navigationMultiplatform = "2.8.0-alpha13" +composeMultiplatform = "1.8.0-beta02" +lifecycleMultiplatform = "2.9.0-alpha06" +navigationMultiplatform = "2.9.0-alpha16" #JetBrains kotlinx-coroutines = "1.10.1" -kotlinxCollectionsImmutable = "0.3.5" +kotlinxCollectionsImmutable = "0.3.8" kotlinxSerializationJson = "1.8.0" -ktor = "3.1.0" -kotlinx-datetime = "0.6.1" +ktor = "3.1.1" +kotlinx-datetime = "0.6.2" #multiplatform -ksoup = "0.2.1" +ksoup = "0.2.2" kermit = "2.0.5" -ktorfit = "2.2.0" +ktorfit = "2.4.1" kotlinInject = "0.7.2" androidx-annotation = "1.9.1" coil = "3.1.0" -datastorePreferences = "1.1.2" +datastorePreferences = "1.1.4" multiplatformSettings = "1.3.0" filekitCompose = "0.8.8" krop = "0.2.0-alpha01" #android -accompanistSystemuicontroller = "0.34.0" -activityCompose = "1.10.0" -androidImageCropper = "4.5.0" +accompanistSystemuicontroller = "0.36.0" +activityCompose = "1.10.1" +androidImageCropper = "4.6.0" browser = "1.8.0" -coreKtx = "1.15.0" +coreKtx = "1.16.0" glance = "1.1.1" material = "1.12.0" -media3 = "1.5.1" -livedata = "1.7.7" +media3 = "1.6.0" 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" + [libraries] kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } compose-ui-graphics = { module = "org.jetbrains.compose.ui:ui-graphics", version.ref = "composeMultiplatform" } -androidx-lifecycle-compiler = { module = "androidx.lifecycle:lifecycle-compiler", version.ref = "lifecycle" } 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-savedstate = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycleMultiplatform" } @@ -81,6 +85,9 @@ 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" } @@ -91,7 +98,6 @@ androidx-glance-material3 = { module = "androidx.glance:glance-material3", versi 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-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "livedata" } 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" } @@ -102,6 +108,9 @@ multiplatform-settings-datastore = { module = "com.russhwolf:multiplatform-setti okio = { module = "com.squareup.okio:okio", version.ref = "okio" } filekit-compose = { module = "io.github.vinceglb:filekit-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" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c57be855..33918ab3 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.12.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 8ccc32b9..9921c142 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -99,8 +99,9 @@ A93A952F29CC810C00F8E227 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1420; + LastUpgradeCheck = 1620; TargetAttributes = { A93A953629CC810C00F8E227 = { CreatedOnToolsVersion = 14.2; @@ -108,7 +109,6 @@ }; }; buildConfigurationList = A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */; - compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -116,6 +116,7 @@ Base, ); mainGroup = A93A952E29CC810C00F8E227; + preferredProjectObjectVersion = 77; productRefGroup = A93A953829CC810C00F8E227 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -205,6 +206,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -265,6 +267,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -291,17 +294,20 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = 4FA7X6639Y; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIRequiresFullScreen = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.daniebeler.pfpixelix.iosApp; PRODUCT_NAME = Pixelix; SWIFT_EMIT_LOC_STRINGS = YES; @@ -318,17 +324,20 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = 4FA7X6639Y; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIRequiresFullScreen = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.daniebeler.pfpixelix.iosApp; PRODUCT_NAME = Pixelix; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x 1.png similarity index 100% rename from iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png rename to iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x 1.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json index c77c4af5..61e90c68 100644 --- a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,50 +1,104 @@ { "images" : [ { - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "20x20" }, { - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "20x20" }, { - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "29x29" }, { - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "29x29" }, { - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "38x38" + }, + { + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "38x38" + }, + { + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "40x40" }, { - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "40x40" }, { - "filename" : "AppIcon@2x.png", - "idiom" : "iphone", + "filename" : "AppIcon@2x 1.png", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "60x60" }, { "filename" : "AppIcon@3x.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "60x60" }, { - "idiom" : "ios-marketing", - "scale" : "1x", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "64x64" + }, + { + "filename" : "pixelix_logo_192.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "64x64" + }, + { + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "68x68" + }, + { + "filename" : "pixelix_logo_152.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "pixelix_logo_167.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "pixelix_logo_black.png", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_152.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_152.png new file mode 100644 index 00000000..94439779 Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_152.png differ diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_167.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_167.png new file mode 100644 index 00000000..06bc3634 Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_167.png differ diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_192.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_192.png new file mode 100644 index 00000000..2fc41a62 Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_192.png differ diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_black.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_black.png new file mode 100644 index 00000000..4661593c Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/pixelix_logo_black.png differ diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index a37c4ab0..c4f50d8f 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -5,13 +5,13 @@ CADisableMinimumFrameDurationOnPhone CFBundleURLTypes - - - CFBundleURLSchemes - - pixelix-android-auth - - - + + + CFBundleURLSchemes + + pixelix-android-auth + + + diff --git a/metadata/ar-SA/full_description.txt b/metadata/ar-SA/full_description.txt index 18d07801..697f5096 100644 --- a/metadata/ar-SA/full_description.txt +++ b/metadata/ar-SA/full_description.txt @@ -1 +1 @@ -Pixelix provides a smooth and intuitive interface for interacting with Pixelfed, the federated image-sharing social network. Designed with user experience in mind, Pixelix makes it simple to connect to your Pixelfed instance, upload photos directly from your device, and browse through your feed with ease. Whether you're a seasoned Pixelfed user or just getting started, Pixelix offers a streamlined way to share and discover visual content. \ No newline at end of file +يوفر بيكسيليكس واجهة سلسة وبديهية للتفاعل مع بيكسلفد، الشبكة الإجتماعية الاتحادية لتشارك الصور. Designed with user experience in mind, Pixelix makes it simple to connect to your Pixelfed instance, upload photos directly from your device, and browse through your feed with ease. سواء كنت مستخدما بيكسلفيد قديم أو فقط مبتدئ، يوفر بيكسيلكس طريقة مبسطة لمشاركة واكتشاف المحتوى البصري. \ No newline at end of file diff --git a/metadata/ar-SA/short_description.txt b/metadata/ar-SA/short_description.txt index 7b39709e..f675510e 100644 --- a/metadata/ar-SA/short_description.txt +++ b/metadata/ar-SA/short_description.txt @@ -1 +1 @@ -Pixelix: a user-friendly Pixelfed client for photo uploads, browsing, & sharing. \ No newline at end of file +Pixelix: عميل Pixelfed سهل الاستخدام لتحميل الصور والتصفح والمشاركة. \ No newline at end of file diff --git a/metadata/da-DK/full_description.txt b/metadata/da-DK/full_description.txt index 18d07801..d0cb6546 100644 --- a/metadata/da-DK/full_description.txt +++ b/metadata/da-DK/full_description.txt @@ -1 +1 @@ -Pixelix provides a smooth and intuitive interface for interacting with Pixelfed, the federated image-sharing social network. Designed with user experience in mind, Pixelix makes it simple to connect to your Pixelfed instance, upload photos directly from your device, and browse through your feed with ease. Whether you're a seasoned Pixelfed user or just getting started, Pixelix offers a streamlined way to share and discover visual content. \ No newline at end of file +Pixelix giver en smidig og intuitiv grænseflade til at interagere med Pixelfed, det fødererede sociale netværk til billeddeling. Pixelix er designet med brugeroplevelsen i tankerne og gør det nemt at oprette forbindelse til din Pixelfed-instans, uploade fotos direkte fra din enhed og gennemse dit feed med lethed. Uanset om du er en erfaren Pixelfed-bruger eller lige er kommet i gang, tilbyder Pixelix en strømlinet måde at dele og opdage visuelt indhold på. \ No newline at end of file diff --git a/metadata/da-DK/short_description.txt b/metadata/da-DK/short_description.txt index 7b39709e..cb0e997e 100644 --- a/metadata/da-DK/short_description.txt +++ b/metadata/da-DK/short_description.txt @@ -1 +1 @@ -Pixelix: a user-friendly Pixelfed client for photo uploads, browsing, & sharing. \ No newline at end of file +Pixelix: en brugervenlig Billedklient til fotooverførsler, gennemsyn og deling. \ No newline at end of file diff --git a/metadata/de-DE/short_description.txt b/metadata/de-DE/short_description.txt index 10dd00d5..bb19032e 100644 --- a/metadata/de-DE/short_description.txt +++ b/metadata/de-DE/short_description.txt @@ -1 +1 @@ -Pixelix: Ein benutzerfreundlicher Pixelfed-Client zum Hochladen, Durchstöbern und Teilen von Fotos. \ No newline at end of file +Pixelix: Ein benutzerfreundlicher Pixelfed Client. \ No newline at end of file diff --git a/metadata/de/changelogs/25.txt b/metadata/de/changelogs/25.txt index a1471545..1ff01a1d 100644 --- a/metadata/de/changelogs/25.txt +++ b/metadata/de/changelogs/25.txt @@ -6,5 +6,4 @@ Fehlerbehebungen: - Ein Problem wurde behoben, bei dem keine Beiträge angezeigt wurden, wenn private Profile betrachtet wurden. - Liken und Boost von rebloggeden Beiträgen: - Ein Fehler wurde behoben, durch den das Liken und Boosten von erneut geposteten Beiträgen nicht funktionierte. - - Toolbar-Insets im Einstellungsbildschirm: - - Falsche Fenster-Insets für die Toolbar im Einstellungsbildschirm wurden korrigiert. \ No newline at end of file + - Toolbar-Insets im Einstellungsbildschirm fixes diff --git a/metadata/de/changelogs/31.txt b/metadata/de/changelogs/31.txt new file mode 100644 index 00000000..994168e2 --- /dev/null +++ b/metadata/de/changelogs/31.txt @@ -0,0 +1,8 @@ + - Sammlungs-Bug behoben + - Mehrere Bilder behalten dasselbe Seitenverhältnis + - Einstellung zum automatischen Anzeigen sensibler Inhalte + - Doppelklick auf das Suchsymbol fokussiert die Suchleiste + - Autovervollständigung für Server-URL am Login-Bildschirm + - Auffälligere Boost-Schaltfläche + - Verbesserte Handhabung von Bild-Popups + - Viele Fehlerbehebungen \ No newline at end of file diff --git a/metadata/el-GR/full_description.txt b/metadata/el-GR/full_description.txt index 18d07801..d4853493 100644 --- a/metadata/el-GR/full_description.txt +++ b/metadata/el-GR/full_description.txt @@ -1 +1 @@ -Pixelix provides a smooth and intuitive interface for interacting with Pixelfed, the federated image-sharing social network. Designed with user experience in mind, Pixelix makes it simple to connect to your Pixelfed instance, upload photos directly from your device, and browse through your feed with ease. Whether you're a seasoned Pixelfed user or just getting started, Pixelix offers a streamlined way to share and discover visual content. \ No newline at end of file +Το Pixelix παρέχει μια ομαλή και διαισθητική διεπαφή για την αλληλεπίδραση με το Pixelfed, το ομοσπονδιακό κοινωνικό δίκτυο διαμοιρασμού εικόνων. Σχεδιασμένο με γνώμονα την εμπειρία χρήστη, το Pixelix καθιστά εύκολη τη σύνδεση με την Pixelfed οντότητά σας, να ανεβάστε φωτογραφίες απευθείας από τη συσκευή σας και να περιηγηθείτε στην ροή σας με ευκολία. Είτε είστε έμπειρος χρήστης Pixelfed είτε μόλις ξεκινήσατε, το Pixelix προσφέρει έναν βελτιωμένο τρόπο να μοιραστείτε και να ανακαλύψετε οπτικό περιεχόμενο. \ No newline at end of file diff --git a/metadata/el-GR/short_description.txt b/metadata/el-GR/short_description.txt index 7b39709e..8fdcbcac 100644 --- a/metadata/el-GR/short_description.txt +++ b/metadata/el-GR/short_description.txt @@ -1 +1 @@ -Pixelix: a user-friendly Pixelfed client for photo uploads, browsing, & sharing. \ No newline at end of file +Pixelix: ένας φιλικός προς το χρήστη πελάτης Pixelfed για μεταφόρτωση φωτογραφιών, περιήγηση & κοινή χρήση. \ No newline at end of file diff --git a/metadata/en-US/changelogs/31.txt b/metadata/en-US/changelogs/31.txt new file mode 100644 index 00000000..3a1c1fa0 --- /dev/null +++ b/metadata/en-US/changelogs/31.txt @@ -0,0 +1,8 @@ + - collection bug fix + - multiple images keep same aspect ratio + - setting to automatically show sensitive content + - double click on search icon focuses search bar + - autocomplete for server url at login screen + - more visible boosted button + - improved image popup handling + - many bug fixes diff --git a/metadata/es-ES/full_description.txt b/metadata/es-ES/full_description.txt index 18d07801..a8f85ced 100644 --- a/metadata/es-ES/full_description.txt +++ b/metadata/es-ES/full_description.txt @@ -1 +1 @@ -Pixelix provides a smooth and intuitive interface for interacting with Pixelfed, the federated image-sharing social network. Designed with user experience in mind, Pixelix makes it simple to connect to your Pixelfed instance, upload photos directly from your device, and browse through your feed with ease. Whether you're a seasoned Pixelfed user or just getting started, Pixelix offers a streamlined way to share and discover visual content. \ No newline at end of file +Pixelix proporciona una interfaz fluida e intuitiva para interactuar con Pixelfed, la red social de intercambio de imágenes federada. Diseñado pensando en la experiencia del usuario, Pixelix simplifica la conexión a tu instancia de Pixelfed, la carga de fotos directamente desde tu dispositivo y la navegación por tu feed con facilidad. Ya seas un usuario experimentado de Pixelfed o solo estés empezando, Pixelix ofrece una forma simplificada de compartir y descubrir contenido visual. \ No newline at end of file diff --git a/metadata/es-ES/short_description.txt b/metadata/es-ES/short_description.txt index 7b39709e..b2592bae 100644 --- a/metadata/es-ES/short_description.txt +++ b/metadata/es-ES/short_description.txt @@ -1 +1 @@ -Pixelix: a user-friendly Pixelfed client for photo uploads, browsing, & sharing. \ No newline at end of file +Pixelix: un cliente de Pixelfed fácil de usar para subir fotos, navegar y compartir. \ No newline at end of file diff --git a/metadata/it-IT/full_description.txt b/metadata/it-IT/full_description.txt index 18d07801..c469a09c 100644 --- a/metadata/it-IT/full_description.txt +++ b/metadata/it-IT/full_description.txt @@ -1 +1 @@ -Pixelix provides a smooth and intuitive interface for interacting with Pixelfed, the federated image-sharing social network. Designed with user experience in mind, Pixelix makes it simple to connect to your Pixelfed instance, upload photos directly from your device, and browse through your feed with ease. Whether you're a seasoned Pixelfed user or just getting started, Pixelix offers a streamlined way to share and discover visual content. \ No newline at end of file +Pixelix offre un'interfaccia intuitiva e scorrevole per interagire con Pixelfed, il social network federato di condivisione foto. Progettato con in mente l'esperienza utente, Pixelix rende semplice la connessione alla tua istanza Pixelfed, caricare direttamente le foto dal tuo dispositivo e navigare con facilità nel tuo feed. Che tu sia un utente Pixelfed con esperienza o che tua abbia appena iniziato, Pixelix offre un modo semplice per scoprire e condividere contenuti visivi. \ No newline at end of file diff --git a/metadata/it-IT/short_description.txt b/metadata/it-IT/short_description.txt index 7b39709e..4961f5cb 100644 --- a/metadata/it-IT/short_description.txt +++ b/metadata/it-IT/short_description.txt @@ -1 +1 @@ -Pixelix: a user-friendly Pixelfed client for photo uploads, browsing, & sharing. \ No newline at end of file +Pixelix: un client di Pixelfed facile da usare per navigare, caricare e condividere foto. \ No newline at end of file diff --git a/metadata/pl-PL/full_description.txt b/metadata/pl-PL/full_description.txt index 925a60ad..8d86c73e 100644 --- a/metadata/pl-PL/full_description.txt +++ b/metadata/pl-PL/full_description.txt @@ -1 +1 @@ -Pixelix zapewnia płynny i intuicyjny interfejs do interakcji z Pixelfed, Federacyjną siecią społecznościową do udostępniania obrazów. Zaprojektowany z myślą o doświadczeniu użytkownika, Pixelux ułatwia łączenie się z Pixelfed instance, przesyłaj zdjęcia bezpośrednio z urządzenia i z łatwością przeglądaj swój kanał. Niezależnie od tego, czy jesteś doświadczonym użytkownikiem Pixelfed, czy dopiero zaczynasz, Pixelix oferuje usprawniony sposób udostępniania i odkrywania treści wizualnych. \ No newline at end of file +Pixelix zapewnia płynny i intuicyjny interfejs do interakcji z Pixelfed, sfederowaną siecią społecznościową do udostępniania obrazów. Zaprojektowany z myślą o doświadczeniu użytkownika, Pixelix ułatwia łączenie się z twoim serwerem Pixelfed, przesyłanie zdjęć bezpośrednio z urządzenia i przeglądanie osi czasu. Niezależnie od tego, czy jesteś doświadczonym użytkownikiem Pixelfed, czy dopiero zaczynasz, Pixelix oferuje prosty sposób udostępniania i odkrywania treści wizualnych. \ No newline at end of file diff --git a/metadata/pl-PL/short_description.txt b/metadata/pl-PL/short_description.txt index 020cdc84..1771272c 100644 --- a/metadata/pl-PL/short_description.txt +++ b/metadata/pl-PL/short_description.txt @@ -1 +1 @@ -Pixelix: przyjazny dla użytkownika Klient Pixelfed do przesyłania zdjęć, przeglądania i udostępniania. \ No newline at end of file +Pixelix: przyjazny klient Pixelfed do udostępniania i przeglądania zdjęć. \ No newline at end of file diff --git a/metadata/pt-PT/full_description.txt b/metadata/pt-PT/full_description.txt index 18d07801..40c809cf 100644 --- a/metadata/pt-PT/full_description.txt +++ b/metadata/pt-PT/full_description.txt @@ -1 +1 @@ -Pixelix provides a smooth and intuitive interface for interacting with Pixelfed, the federated image-sharing social network. Designed with user experience in mind, Pixelix makes it simple to connect to your Pixelfed instance, upload photos directly from your device, and browse through your feed with ease. Whether you're a seasoned Pixelfed user or just getting started, Pixelix offers a streamlined way to share and discover visual content. \ No newline at end of file +O Pixelix fornece uma interface suave e intuitiva para interagir com o Pixelfed, a rede social federada de partilha de imagens. Concebido a pensar na experiência do utilizador, o Pixelix facilita a ligação à sua instância Pixelfed, o o envio de fotografias diretamente do seu dispositivo e a navegação pela sua cronologia com facilidade. Quer seja um utilizador experiente do Pixelfed ou esteja apenas a começar, o Pixelix oferece uma forma simplificada de partilhar e descobrir conteúdos visuais. \ No newline at end of file diff --git a/metadata/pt-PT/short_description.txt b/metadata/pt-PT/short_description.txt index 7b39709e..d5a6a0a2 100644 --- a/metadata/pt-PT/short_description.txt +++ b/metadata/pt-PT/short_description.txt @@ -1 +1 @@ -Pixelix: a user-friendly Pixelfed client for photo uploads, browsing, & sharing. \ No newline at end of file +Pixelix: um cliente Pixelfed de fácil utilização para carregar, navegar e partilhar fotografias. \ No newline at end of file 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") } }