diff --git a/app/src/androidMain/AndroidManifest.xml b/app/src/androidMain/AndroidManifest.xml
index 272a6d33..32eb074d 100644
--- a/app/src/androidMain/AndroidManifest.xml
+++ b/app/src/androidMain/AndroidManifest.xml
@@ -17,12 +17,24 @@
android:theme="@style/Theme.Pixelix"
tools:ignore="Instantiatable"
tools:targetApi="31">
-
+
+
+
+
+
+ android:windowSoftInputMode="adjustResize"
+ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+ android:launchMode="singleInstance">
@@ -51,7 +63,16 @@
+
+
+
+
+
+
+
+ android:targetActivity=".AppActivity">
@@ -73,7 +94,7 @@
android:enabled="false"
android:icon="@mipmap/ic_launcher_05"
android:roundIcon="@mipmap/ic_launcher_05_round"
- android:targetActivity=".MainActivity">
+ android:targetActivity=".AppActivity">
@@ -86,7 +107,7 @@
android:enabled="false"
android:icon="@mipmap/ic_launcher_06"
android:roundIcon="@mipmap/ic_launcher_06_round"
- android:targetActivity=".MainActivity">
+ android:targetActivity=".AppActivity">
@@ -99,7 +120,7 @@
android:enabled="false"
android:icon="@mipmap/ic_launcher_07"
android:roundIcon="@mipmap/ic_launcher_07_round"
- android:targetActivity=".MainActivity">
+ android:targetActivity=".AppActivity">
@@ -112,7 +133,7 @@
android:enabled="false"
android:icon="@mipmap/ic_launcher_08"
android:roundIcon="@mipmap/ic_launcher_08_round"
- android:targetActivity=".MainActivity">
+ android:targetActivity=".AppActivity">
@@ -125,7 +146,7 @@
android:enabled="false"
android:icon="@mipmap/ic_launcher_09"
android:roundIcon="@mipmap/ic_launcher_09_round"
- android:targetActivity=".MainActivity">
+ android:targetActivity=".AppActivity">
@@ -138,7 +159,7 @@
android:enabled="false"
android:icon="@mipmap/ic_launcher_01"
android:roundIcon="@mipmap/ic_launcher_01_round"
- android:targetActivity=".MainActivity">
+ android:targetActivity=".AppActivity">
@@ -151,32 +172,13 @@
android:enabled="false"
android:icon="@mipmap/ic_launcher_03"
android:roundIcon="@mipmap/ic_launcher_03_round"
- android:targetActivity=".MainActivity">
+ android:targetActivity=".AppActivity">
-
-
-
-
-
-
-
-
-
-
-
{
+ intent.dataString?.let { onExternalUrl(it) }
+ }
+ Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE -> {
+ val imageUris = handleSharePhotoIntent(intent, contentResolver, cacheDir)
+ if (imageUris.isNotEmpty()) {
+ imageUris.forEach { uri ->
+ try {
+ contentResolver.takePersistableUriPermission(
+ uri, Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+ } catch (e: SecurityException) {
+ e.printStackTrace() // Handle permission denial gracefully
+ }
+ }
+ onExternalFileShare(imageUris)
+ }
+ }
+ }
+ }
+
+ private fun onExternalUrl(url: String) {
+ val systemUrlHandler = MyApplication.appComponent.systemUrlHandler
+ systemUrlHandler.onRedirect(url)
+ }
+
+ private fun onExternalFileShare(uris: List) {
+ val systemFileShare = MyApplication.appComponent.systemFileShare
+ systemFileShare.share(uris)
+ }
+}
+
+@Composable
+actual 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
+ }
+}
+
+private fun saveUriToCache(uri: Uri, contentResolver: ContentResolver, cacheDir: File): Uri? {
+ try {
+ val inputStream: InputStream? = contentResolver.openInputStream(uri)
+ inputStream?.use { input ->
+ val file = File(cacheDir, "shared_image_${System.currentTimeMillis()}.jpg")
+ FileOutputStream(file).use { output ->
+ input.copyTo(output)
+ }
+ return Uri.fromFile(file) // Return the new cached URI
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ return null
+}
+
+private fun handleSharePhotoIntent(
+ intent: Intent, contentResolver: ContentResolver, cacheDir: File
+): List {
+ val action = intent.action
+ val type = intent.type
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+
+ var imageUris: List = emptyList()
+ when {
+ Intent.ACTION_SEND == action && type != null -> {
+ if (type.startsWith("image/") || type.startsWith("video/")) {
+ val singleUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(
+ Intent.EXTRA_STREAM, Uri::class.java
+ )
+ } else {
+ @Suppress("DEPRECATION") intent.getParcelableExtra(
+ Intent.EXTRA_STREAM
+ ) as? Uri
+ }
+ singleUri?.let { uri ->
+ val cachedUri = saveUriToCache(uri, contentResolver, cacheDir)
+ imageUris =
+ cachedUri?.let { listOf(it) } ?: emptyList() // Wrap single image in a list
+ }
+ }
+ }
+
+ Intent.ACTION_SEND_MULTIPLE == action && type != null -> {
+ val receivedUris = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableArrayListExtra(
+ Intent.EXTRA_STREAM, Uri::class.java
+ )
+ } else {
+ @Suppress("DEPRECATION") intent.getParcelableArrayListExtra(
+ Intent.EXTRA_STREAM
+ )
+ }
+ imageUris = receivedUris?.mapNotNull {
+ saveUriToCache(
+ it, contentResolver, cacheDir
+ )
+ } ?: emptyList()
+ }
+ }
+ return imageUris
+}
\ No newline at end of file
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/LoginActivity.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/LoginActivity.kt
deleted file mode 100644
index 5aebb337..00000000
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/LoginActivity.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-package com.daniebeler.pfpixelix
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.compositionLocalOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.core.view.WindowCompat
-import com.daniebeler.pfpixelix.common.Resource
-import com.daniebeler.pfpixelix.di.EntryPointComponent
-import com.daniebeler.pfpixelix.di.HostSelectionInterceptorInterface
-import com.daniebeler.pfpixelix.di.LocalAppComponent
-import com.daniebeler.pfpixelix.di.create
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.usecase.AddNewLoginUseCase
-import com.daniebeler.pfpixelix.domain.usecase.FinishLoginUseCase
-import com.daniebeler.pfpixelix.domain.usecase.GetOngoingLoginUseCase
-import com.daniebeler.pfpixelix.domain.usecase.ObtainTokenUseCase
-import com.daniebeler.pfpixelix.domain.usecase.UpdateLoginDataUseCase
-import com.daniebeler.pfpixelix.domain.usecase.VerifyTokenUseCase
-import com.daniebeler.pfpixelix.ui.composables.LoginComposable
-import com.daniebeler.pfpixelix.ui.theme.PixelixTheme
-import com.daniebeler.pfpixelix.utils.LocalKmpContext
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-class LoginActivity : ComponentActivity() {
- lateinit var obtainTokenUseCase: ObtainTokenUseCase
- lateinit var verifyTokenUseCase: VerifyTokenUseCase
- lateinit var updateLoginDataUseCase: UpdateLoginDataUseCase
- lateinit var finishLoginUseCase: FinishLoginUseCase
- lateinit var newLoginDataUseCase: AddNewLoginUseCase
- lateinit var getOngoingLoginUseCase: GetOngoingLoginUseCase
- lateinit var hostSelectionInterceptorInterface: HostSelectionInterceptorInterface
-
-
- private var isLoadingAfterRedirect: Boolean by mutableStateOf(false)
- private var error: String by mutableStateOf("")
-
- override fun onCreate(savedInstanceState: Bundle?) {
- EntryPointComponent::class.create(MyApplication.appComponent).let {
- obtainTokenUseCase = it.obtainTokenUseCase
- verifyTokenUseCase = it.verifyTokenUseCase
- updateLoginDataUseCase = it.updateLoginDataUseCase
- finishLoginUseCase = it.finishLoginUseCase
- newLoginDataUseCase = it.newLoginDataUseCase
- getOngoingLoginUseCase = it.getOngoingLoginUseCase
- hostSelectionInterceptorInterface = it.hostSelectionInterceptorInterface
- }
-
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- WindowCompat.setDecorFitsSystemWindows(window, false)
-
-
- setContent {
- CompositionLocalProvider(
- LocalAppComponent provides MyApplication.appComponent,
- LocalKmpContext provides this,
- ) {
- PixelixTheme(
- dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
- ) {
- Scaffold { paddingValues ->
- Column(Modifier.padding(paddingValues)) {
-
- }
- LoginComposable(isLoadingAfterRedirect, error)
- }
- }
- }
- }
- }
-
- override fun onStart() {
- super.onStart()
- val extras = intent.extras
- if (extras != null) {
- val baseUrl = extras.getString("base_url")
- val accessToken = extras.getString("access_token")
- if (baseUrl != null && accessToken != null) {
- hostSelectionInterceptorInterface.setHost(baseUrl)
- hostSelectionInterceptorInterface.setToken(accessToken)
- CoroutineScope(Dispatchers.Default).launch {
- verifyToken(LoginData(baseUrl = baseUrl, accessToken = accessToken), true)
- }
- }
- }
-
- val url: Uri? = intent.data
-
- //Check if the activity was started after the authentication
- if (url == null || !url.toString().startsWith("pixelix-android-auth://callback")) return
-
- val code = url.getQueryParameter("code") ?: ""
-
-
- if (code.isNotEmpty()) {
-
- isLoadingAfterRedirect = true
- CoroutineScope(Dispatchers.Default).launch {
- getTokenAndRedirect(code)
- }
- }
- }
-
- private suspend fun getTokenAndRedirect(code: String) {
- val loginData: LoginData? = getOngoingLoginUseCase()
- if (loginData == null) {
- error = "an unexpected error occured"
- isLoadingAfterRedirect = false
- } else {
- obtainTokenUseCase(loginData.clientId, loginData.clientSecret, code).collect { result ->
- when (result) {
- is Resource.Success -> {
- val newLoginData = loginData.copy(accessToken = result.data!!.accessToken)
- updateLoginDataUseCase(newLoginData)
- verifyToken(newLoginData, false)
- }
-
- is Resource.Error -> {
- error = result.message ?: "Error"
- isLoadingAfterRedirect = false
- }
-
- is Resource.Loading -> {
- isLoadingAfterRedirect = true
- }
- }
- }
- }
- }
-
- private suspend fun verifyToken(loginData: LoginData, updateToAuthV2: Boolean) {
- verifyTokenUseCase(loginData.accessToken).collect { result ->
- when (result) {
- is Resource.Success -> {
- if (result.data == null) {
- error = "an unexpected error occured"
- isLoadingAfterRedirect = false
- return@collect
- }
- val newLoginData = loginData.copy(
- accountId = result.data.id,
- username = result.data.username,
- avatar = result.data.avatar,
- displayName = result.data.displayname,
- followers = result.data.followersCount,
- loginOngoing = false
- )
- if (updateToAuthV2) {
- newLoginDataUseCase.invoke(newLoginData)
- }
- finishLoginUseCase(newLoginData, newLoginData.accountId)
-
- redirect()
- }
-
- is Resource.Error -> {
- error = result.message ?: "Error"
- isLoadingAfterRedirect = false
- }
-
- is Resource.Loading -> {
- isLoadingAfterRedirect = true
- }
- }
- }
- }
-
- private fun redirect() {
- val intent = Intent(applicationContext, MainActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
- applicationContext.startActivity(intent)
- }
-}
\ No newline at end of file
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MainActivity.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MainActivity.kt
index d7d75b85..bd5e21a4 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MainActivity.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MainActivity.kt
@@ -1,619 +1,157 @@
-package com.daniebeler.pfpixelix
-
-import android.content.ContentResolver
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-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
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.UnfoldMore
-import androidx.compose.material3.DrawerValue
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.MaterialTheme.shapes
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.ModalDrawerSheet
-import androidx.compose.material3.NavigationBar
-import androidx.compose.material3.NavigationBarItem
-import androidx.compose.material3.NavigationBarItemDefaults
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.rememberDrawerState
-import androidx.compose.material3.rememberModalBottomSheetState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-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.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import org.jetbrains.compose.resources.painterResource
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.dp
-import androidx.core.view.WindowCompat
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.currentBackStackEntryAsState
-import androidx.navigation.compose.rememberNavController
-import androidx.navigation.navArgument
-import co.touchlab.kermit.Logger
-import coil3.compose.AsyncImage
-import com.daniebeler.pfpixelix.common.Destinations
-import com.daniebeler.pfpixelix.di.EntryPointComponent
-import com.daniebeler.pfpixelix.di.HostSelectionInterceptorInterface
-import com.daniebeler.pfpixelix.di.LocalAppComponent
-import com.daniebeler.pfpixelix.di.create
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.repository.CountryRepository
-import com.daniebeler.pfpixelix.domain.usecase.GetCurrentLoginDataUseCase
-import com.daniebeler.pfpixelix.domain.usecase.VerifyTokenUseCase
-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.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.theme.PixelixTheme
-import com.daniebeler.pfpixelix.utils.LocalKmpContext
-import com.daniebeler.pfpixelix.utils.Navigate
-import com.daniebeler.pfpixelix.utils.end
-import com.daniebeler.pfpixelix.utils.openLoginScreen
-import kotlinx.coroutines.cancelChildren
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.serialization.json.Json
-import org.jetbrains.compose.resources.stringResource
-import org.jetbrains.compose.resources.vectorResource
-import pixelix.app.generated.resources.Res
-import pixelix.app.generated.resources.default_avatar
-import java.io.File
-import java.io.FileOutputStream
-import java.io.InputStream
-
-
-class MainActivity : ComponentActivity() {
- lateinit var currentLoginDataUseCase: GetCurrentLoginDataUseCase
- lateinit var hostSelectionInterceptorInterface: HostSelectionInterceptorInterface
- lateinit var repository: CountryRepository
- lateinit var verifyTokenUseCase: VerifyTokenUseCase
-
- companion object {
- const val KEY_DESTINATION: String = "destination"
- const val KEY_DESTINATION_PARAM: String = "destination_parameter"
-
- enum class StartNavigation {
- Notifications, Profile, Post
- }
- }
-
-
- @OptIn(ExperimentalMaterial3Api::class)
- override fun onCreate(savedInstanceState: Bundle?) {
- EntryPointComponent::class.create(MyApplication.appComponent).let {
- currentLoginDataUseCase = it.currentLoginDataUseCase
- hostSelectionInterceptorInterface = it.hostSelectionInterceptorInterface
- repository = it.repository
- verifyTokenUseCase = it.verifyTokenUseCase
- }
-
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- WindowCompat.setDecorFitsSystemWindows(window, false)
- var avatar = ""
- runBlocking {
- val loginData: LoginData? = currentLoginDataUseCase()
- if (loginData == null || loginData.accessToken.isBlank() || loginData.baseUrl.isBlank()) {
- val oldBaseurl: String? = repository.getAuthV1Baseurl().firstOrNull()
- val oldAccessToken: String? = repository.getAuthV1Token().firstOrNull()
- if (oldBaseurl != null && oldAccessToken != null && oldBaseurl.isNotBlank() && oldAccessToken.isNotBlank()) {
- repository.deleteAuthV1Data()
- updateAuthToV2(this@MainActivity, oldBaseurl, oldAccessToken)
- } else {
- openLoginScreen()
- }
- } else {
- if (loginData.accessToken.isNotEmpty()) {
- hostSelectionInterceptorInterface.setToken(loginData.accessToken)
- }
- if (loginData.baseUrl.isNotEmpty()) {
- hostSelectionInterceptorInterface.setHost(
- loginData.baseUrl.replace(
- "https://", ""
- )
- )
- }
- avatar = loginData.avatar
- }
- }
-
- val imageUris = handleSharePhotoIntent(intent, contentResolver, cacheDir)
-
-
- setContent {
- CompositionLocalProvider(
- LocalAppComponent provides MyApplication.appComponent,
- LocalKmpContext provides this,
- ) {
- val scope = rememberCoroutineScope()
- val drawerState = rememberDrawerState(DrawerValue.Closed)
-
- val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
- var showAccountSwitchBottomSheet by remember { mutableStateOf(false) }
-
- PixelixTheme(
- dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
- ) {
- val navController: NavHostController = rememberNavController()
- ReverseModalNavigationDrawer(
- gesturesEnabled = drawerState.isOpen,
- drawerState = drawerState,
- drawerContent = {
- ModalDrawerSheet(
- drawerState = drawerState,
- drawerShape = shapes.extraLarge.end(0.dp),
- windowInsets = WindowInsets(0, 0, 0, 0)
- ) {
- PreferencesComposable(navController, drawerState, {
- scope.launch {
- drawerState.close()
- }
- })
- }
- }) {
-
- Scaffold(contentWindowInsets = WindowInsets(0.dp), bottomBar = {
- BottomBar(
- navController = navController,
- avatar = avatar,
- openAccountSwitchBottomSheet = {
- showAccountSwitchBottomSheet = true
- },
- context = this
- )
- }) { paddingValues ->
- Box(
- modifier = Modifier.padding(paddingValues)
- ) {
- NavigationGraph(navController = navController, {
- scope.launch {
- drawerState.open()
- }
- })
-
- LaunchedEffect(imageUris) {
- imageUris.forEach { uri ->
- try {
- contentResolver.takePersistableUriPermission(
- uri, Intent.FLAG_GRANT_READ_URI_PERMISSION
- )
- } catch (e: SecurityException) {
- e.printStackTrace() // Handle permission denial gracefully
- }
- }
- if (imageUris.isNotEmpty()) {
- val urisJson =
- Json.encodeToString(imageUris.map { uri -> uri.toString() })
- Navigate.navigate(
- "new_post_screen?uris=$urisJson", navController
- )
- }
- }
-
- val destination = intent.extras?.getString(KEY_DESTINATION) ?: ""
- if (destination.isNotBlank()) {
- // Delay the navigation action to ensure the graph is set
- LaunchedEffect(Unit) {
- when (destination) {
- StartNavigation.Notifications.toString() -> Navigate.navigate(
- "notifications_screen", navController
- )
-
- StartNavigation.Profile.toString() -> {
- val accountId: String = intent.extras?.getString(
- KEY_DESTINATION_PARAM
- ) ?: ""
- if (accountId.isNotBlank()) {
- Navigate.navigate(
- "profile_screen/$accountId", navController
- )
- }
- }
-
- StartNavigation.Post.toString() -> {
- val postId: String = intent.extras?.getString(
- KEY_DESTINATION_PARAM
- ) ?: ""
- if (postId.isNotBlank()) {
- Navigate.navigate(
- "single_post_screen/$postId", navController
- )
-
- }
- }
- }
- }
- }
- }
-
-
- }
- }
- if (showAccountSwitchBottomSheet) {
- ModalBottomSheet(
- onDismissRequest = {
- showAccountSwitchBottomSheet = false
- }, sheetState = sheetState
- ) {
- AccountSwitchBottomSheet(closeBottomSheet = {
- showAccountSwitchBottomSheet = false
- }, null)
- }
- }
- }
- }
- }
- }
-}
-
-fun saveUriToCache(uri: Uri, contentResolver: ContentResolver, cacheDir: File): Uri? {
- try {
- val inputStream: InputStream? = contentResolver.openInputStream(uri)
- inputStream?.use { input ->
- val file = File(cacheDir, "shared_image_${System.currentTimeMillis()}.jpg")
- FileOutputStream(file).use { output ->
- input.copyTo(output)
- }
- return Uri.fromFile(file) // Return the new cached URI
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
- return null
-}
-
-private fun handleSharePhotoIntent(
- intent: Intent, contentResolver: ContentResolver, cacheDir: File
-): List {
- val action = intent.action
- val type = intent.type
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
-
- var imageUris: List = emptyList()
- when {
- Intent.ACTION_SEND == action && type != null -> {
- if (type.startsWith("image/") || type.startsWith("video/")) {
- val singleUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- intent.getParcelableExtra(
- Intent.EXTRA_STREAM, Uri::class.java
- )
- } else {
- @Suppress("DEPRECATION") intent.getParcelableExtra(
- Intent.EXTRA_STREAM
- ) as? Uri
- }
- singleUri?.let { uri ->
- val cachedUri = saveUriToCache(uri, contentResolver, cacheDir)
- imageUris =
- cachedUri?.let { listOf(it) } ?: emptyList() // Wrap single image in a list
- }
- }
- }
-
- Intent.ACTION_SEND_MULTIPLE == action && type != null -> {
- val receivedUris = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- intent.getParcelableArrayListExtra(
- Intent.EXTRA_STREAM, Uri::class.java
- )
- } else {
- @Suppress("DEPRECATION") intent.getParcelableArrayListExtra(
- Intent.EXTRA_STREAM
- )
- }
- imageUris = receivedUris?.mapNotNull {
- saveUriToCache(
- it, contentResolver, cacheDir
- )
- } ?: emptyList()
- }
- }
- return imageUris
-}
-
-fun updateAuthToV2(context: Context, baseUrl: String, accessToken: String) {
- val intent = Intent(context, LoginActivity::class.java)
- intent.putExtra("base_url", baseUrl)
- intent.putExtra("access_token", accessToken)
- context.startActivity(intent)
-}
-
-@Composable
-fun NavigationGraph(
- navController: NavHostController,
- openPreferencesDrawer: () -> Unit
-) {
- NavHost(navController,
- startDestination = Destinations.HomeScreen.route,
- enterTransition = { EnterTransition.None },
- exitTransition = { ExitTransition.None }) {
- composable(Destinations.HomeScreen.route) {
- HomeComposable(navController, openPreferencesDrawer)
- }
-
- composable(Destinations.NotificationsScreen.route) {
- NotificationsComposable(navController)
- }
-
- composable(Destinations.Profile.route) { navBackStackEntry ->
- val uId = navBackStackEntry.arguments?.getString("userid")
-
- uId?.let { id ->
- OtherProfileComposable(navController, userId = id, byUsername = null)
-
- }
- }
-
- composable(Destinations.ProfileByUsername.route) { navBackStackEntry ->
- val username = navBackStackEntry.arguments?.getString("username")
-
- username?.let {
- OtherProfileComposable(navController, userId = "", byUsername = it)
- }
- }
-
- composable(Destinations.Hashtag.route) { navBackStackEntry ->
- 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?.getString("uris")
- val imageUris: List? = urisJson?.let { json ->
- Json.decodeFromString>(json).map { Uri.parse(it) }
- }
- NewPostComposable(navController, imageUris)
- }
-
- composable(Destinations.EditPost.route) { navBackStackEntry ->
- 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?.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?.getString("postid")
- val refresh = navBackStackEntry.arguments?.getBoolean("refresh")!!
- val openReplies = navBackStackEntry.arguments?.getBoolean("openReplies")!!
- Logger.d("refresh") { refresh.toString() }
- Logger.d("openReplies") { openReplies.toString() }
- uId?.let { id ->
- SinglePostComposable(navController, postId = id, refresh, openReplies)
- }
- }
-
- composable(Destinations.Collection.route) { navBackStackEntry ->
- val uId = navBackStackEntry.arguments?.getString("collectionid")
- uId?.let { id ->
- CollectionComposable(navController, collectionId = id)
- }
- }
-
- composable(Destinations.Search.route) {
- ExploreComposable(navController)
- }
-
- composable(Destinations.Conversation.route) {
- ConversationsComposable(navController = navController)
- }
-
- composable(Destinations.Chat.route) { navBackStackEntry ->
- val uId = navBackStackEntry.arguments?.getString("userid")
- uId?.let { id ->
- ChatComposable(navController = navController, accountId = id)
- }
- }
-
- composable(Destinations.Mention.route) { navBackStackEntry ->
- val mentionId = navBackStackEntry.arguments?.getString("mentionid")
- mentionId?.let { id ->
- MentionComposable(navController = navController, mentionId = id)
- }
- }
- }
-}
-
-@Composable
-fun BottomBar(
- navController: NavHostController,
- avatar: String,
- openAccountSwitchBottomSheet: () -> Unit,
- context: Context
-) {
- val screens = listOf(
- Destinations.HomeScreen,
- Destinations.Search,
- Destinations.NewPost,
- Destinations.NotificationsScreen,
- Destinations.OwnProfile
- )
- val systemNavigationBarHeight =
- WindowInsets.navigationBars.asPaddingValues(Density(context)).calculateBottomPadding()
-
- NavigationBar(
- modifier = Modifier.height(60.dp + systemNavigationBarHeight)
- ) {
- val navBackStackEntry by navController.currentBackStackEntryAsState()
- val currentRoute = navBackStackEntry?.destination?.route
- screens.forEach { screen ->
- val interactionSource = remember { MutableInteractionSource() }
- val coroutineScope = rememberCoroutineScope()
- var isLongPress by remember { mutableStateOf(false) }
-
- LaunchedEffect(interactionSource) {
- interactionSource.interactions.collect { interaction ->
- when (interaction) {
- is PressInteraction.Press -> {
- isLongPress = false // Reset flag before starting detection
- coroutineScope.launch {
- delay(500L) // Long-press threshold
- if (screen.route == Destinations.OwnProfile.route) {
- openAccountSwitchBottomSheet()
- }
- isLongPress = true
- }
- }
-
- is PressInteraction.Release, is PressInteraction.Cancel -> {
- coroutineScope.coroutineContext.cancelChildren()
- }
- }
- }
- }
- NavigationBarItem(icon = {
- if (screen.route == Destinations.OwnProfile.route && avatar.isNotBlank()) {
- 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 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,
- colors = NavigationBarItemDefaults.colors(
- selectedIconColor = MaterialTheme.colorScheme.inverseSurface,
- indicatorColor = Color.Transparent
- ),
- interactionSource = interactionSource,
- onClick = {
- if (!isLongPress) {
- Navigate.navigateWithPopUp(screen.route, navController)
- }
- })
- }
- }
-}
+//todo kmp
+//
+// runBlocking {
+// val loginData: LoginData? = currentLoginDataUseCase()
+// if (loginData == null || loginData.accessToken.isBlank() || loginData.baseUrl.isBlank()) {
+// val oldBaseurl: String? = repository.getAuthV1Baseurl().firstOrNull()
+// val oldAccessToken: String? = repository.getAuthV1Token().firstOrNull()
+// if (oldBaseurl != null && oldAccessToken != null && oldBaseurl.isNotBlank() && oldAccessToken.isNotBlank()) {
+// repository.deleteAuthV1Data()
+// updateAuthToV2(this@MainActivity, oldBaseurl, oldAccessToken)
+// } else {
+// openLoginScreen()
+// }
+// } else {
+// if (loginData.accessToken.isNotEmpty()) {
+// hostSelectionInterceptorInterface.setToken(loginData.accessToken)
+// }
+// if (loginData.baseUrl.isNotEmpty()) {
+// hostSelectionInterceptorInterface.setHost(
+// loginData.baseUrl.replace(
+// "https://", ""
+// )
+// )
+// }
+// avatar = loginData.avatar
+// }
+// }
+//
+// val imageUris = handleSharePhotoIntent(intent, contentResolver, cacheDir)
+//
+//
+// LaunchedEffect(imageUris) {
+// imageUris.forEach { uri ->
+// try {
+// contentResolver.takePersistableUriPermission(
+// uri, Intent.FLAG_GRANT_READ_URI_PERMISSION
+// )
+// } catch (e: SecurityException) {
+// e.printStackTrace() // Handle permission denial gracefully
+// }
+// }
+// if (imageUris.isNotEmpty()) {
+// val urisJson =
+// Json.encodeToString(imageUris.map { uri -> uri.toString() })
+// Navigate.navigate(
+// "new_post_screen?uris=$urisJson", navController
+// )
+// }
+// }
+//
+// val destination = intent.extras?.getString(KEY_DESTINATION) ?: ""
+// if (destination.isNotBlank()) {
+// // Delay the navigation action to ensure the graph is set
+// LaunchedEffect(Unit) {
+// when (destination) {
+// StartNavigation.Notifications.toString() -> Navigate.navigate(
+// "notifications_screen", navController
+// )
+//
+// StartNavigation.Profile.toString() -> {
+// val accountId: String = intent.extras?.getString(
+// KEY_DESTINATION_PARAM
+// ) ?: ""
+// if (accountId.isNotBlank()) {
+// Navigate.navigate(
+// "profile_screen/$accountId", navController
+// )
+// }
+// }
+//
+// StartNavigation.Post.toString() -> {
+// val postId: String = intent.extras?.getString(
+// KEY_DESTINATION_PARAM
+// ) ?: ""
+// if (postId.isNotBlank()) {
+// Navigate.navigate(
+// "single_post_screen/$postId", navController
+// )
+//
+// }
+// }
+// }
+// }
+// }
+// }
+//
+//
+//fun saveUriToCache(uri: Uri, contentResolver: ContentResolver, cacheDir: File): Uri? {
+// try {
+// val inputStream: InputStream? = contentResolver.openInputStream(uri)
+// inputStream?.use { input ->
+// val file = File(cacheDir, "shared_image_${System.currentTimeMillis()}.jpg")
+// FileOutputStream(file).use { output ->
+// input.copyTo(output)
+// }
+// return Uri.fromFile(file) // Return the new cached URI
+// }
+// } catch (e: Exception) {
+// e.printStackTrace()
+// }
+// return null
+//}
+//
+//private fun handleSharePhotoIntent(
+// intent: Intent, contentResolver: ContentResolver, cacheDir: File
+//): List {
+// val action = intent.action
+// val type = intent.type
+// intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+//
+// var imageUris: List = emptyList()
+// when {
+// Intent.ACTION_SEND == action && type != null -> {
+// if (type.startsWith("image/") || type.startsWith("video/")) {
+// val singleUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+// intent.getParcelableExtra(
+// Intent.EXTRA_STREAM, Uri::class.java
+// )
+// } else {
+// @Suppress("DEPRECATION") intent.getParcelableExtra(
+// Intent.EXTRA_STREAM
+// ) as? Uri
+// }
+// singleUri?.let { uri ->
+// val cachedUri = saveUriToCache(uri, contentResolver, cacheDir)
+// imageUris =
+// cachedUri?.let { listOf(it) } ?: emptyList() // Wrap single image in a list
+// }
+// }
+// }
+//
+// Intent.ACTION_SEND_MULTIPLE == action && type != null -> {
+// val receivedUris = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+// intent.getParcelableArrayListExtra(
+// Intent.EXTRA_STREAM, Uri::class.java
+// )
+// } else {
+// @Suppress("DEPRECATION") intent.getParcelableArrayListExtra(
+// Intent.EXTRA_STREAM
+// )
+// }
+// imageUris = receivedUris?.mapNotNull {
+// saveUriToCache(
+// it, contentResolver, cacheDir
+// )
+// } ?: emptyList()
+// }
+// }
+// return imageUris
+//}
+//
+//fun updateAuthToV2(context: Context, baseUrl: String, accessToken: String) {
+// val intent = Intent(context, LoginActivity::class.java)
+// intent.putExtra("base_url", baseUrl)
+// intent.putExtra("access_token", accessToken)
+// context.startActivity(intent)
+//}
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt
index 8357fb17..4bf0a463 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/MyApplication.kt
@@ -21,7 +21,7 @@ class MyApplication : Application(), Configuration.Provider {
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
override fun onCreate() {
- appComponent = AppComponent::class.create(this)
+ appComponent = AppComponent.create(this)
SingletonImageLoader.setSafe {
appComponent.provideImageLoader()
}
@@ -44,7 +44,6 @@ private class MyWorkerFactory(
): ListenableWorker? {
val workerComponent = WorkerComponent::class.create(
appComponent,
- appContext,
workerParameters
)
return when(workerClassName) {
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/di/WorkerComponent.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/di/WorkerComponent.kt
index f908e416..0b260f65 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/di/WorkerComponent.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/di/WorkerComponent.kt
@@ -1,6 +1,5 @@
package com.daniebeler.pfpixelix.di
-import android.content.Context
import androidx.work.WorkerParameters
import com.daniebeler.pfpixelix.widget.notifications.work_manager.LatestImageTask
import com.daniebeler.pfpixelix.widget.notifications.work_manager.NotificationsTask
@@ -10,7 +9,6 @@ import me.tatarka.inject.annotations.Provides
@Component
abstract class WorkerComponent(
@Component val appComponent: AppComponent,
- @get:Provides val context: Context,
@get:Provides val workerParameters: WorkerParameters
) {
abstract val notificationsTask: NotificationsTask
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/GetFile.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/GetFile.android.kt
index 60fe5ced..fb2216b1 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/GetFile.android.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/GetFile.android.kt
@@ -6,19 +6,27 @@ import android.provider.OpenableColumns
actual object GetFile {
- actual fun getFileName(uri: Uri, context: Context): String? = context.contentResolver.query(
- uri, null, null, null, null
- )?.use {
- val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
- it.moveToFirst()
- it.getString(nameIndex)
+ actual fun getFileName(uri: Uri, context: Context): String? = when(uri.scheme) {
+ "file" -> uri.pathSegments.last().substringBeforeLast('.')
+ "content" -> context.contentResolver.query(
+ uri, null, null, null, null
+ )?.use {
+ val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ it.moveToFirst()
+ it.getString(nameIndex)
+ }
+ else -> null
}
- actual fun getFileSize(uri: Uri, context: Context): Long? = context.contentResolver.query(
- uri, null, null, null, null
- )?.use {
- val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)
- it.moveToFirst()
- it.getLong(sizeIndex)
+ actual fun getFileSize(uri: Uri, context: Context): Long? = when(uri.scheme) {
+ "file" -> context.contentResolver.openFileDescriptor(uri, "r")?.use { it.statSize }
+ "content" -> context.contentResolver.query(
+ uri, null, null, null, null
+ )?.use {
+ val nameIndex = it.getColumnIndex(OpenableColumns.SIZE)
+ it.moveToFirst()
+ it.getLong(nameIndex)
+ }
+ else -> null
}
}
\ No newline at end of file
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.android.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.android.kt
index 97054f33..0807dd31 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
@@ -16,7 +16,6 @@ import androidx.core.graphics.drawable.toBitmap
import androidx.core.net.toUri
import co.touchlab.kermit.Logger
import coil3.PlatformContext
-import com.daniebeler.pfpixelix.LoginActivity
import com.daniebeler.pfpixelix.R
import com.daniebeler.pfpixelix.ui.composables.settings.icon_selection.IconWithName
import com.daniebeler.pfpixelix.utils.ThemePrefUtil.AMOLED
@@ -77,14 +76,6 @@ actual fun KmpContext.setDefaultNightMode(mode: Int) {
)
}
-actual fun KmpContext.openLoginScreen(isAbleToGotBack: Boolean) {
- val intent = Intent(this, LoginActivity::class.java)
- if (!isAbleToGotBack) {
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
- }
- startActivity(intent)
-}
-
actual fun KmpContext.getCacheSizeInBytes(): Long {
return cacheDir.walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
}
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/WidgetRepositoryProvider.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/WidgetRepositoryProvider.kt
deleted file mode 100644
index ec323834..00000000
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/WidgetRepositoryProvider.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.daniebeler.pfpixelix.widget
-
-import HostSelectionInterceptor
-import androidx.datastore.core.DataStore
-import co.touchlab.kermit.Logger
-import com.daniebeler.pfpixelix.data.remote.PixelfedApi
-import com.daniebeler.pfpixelix.data.remote.createPixelfedApi
-import com.daniebeler.pfpixelix.data.repository.WidgetRepositoryImpl
-import com.daniebeler.pfpixelix.domain.model.AuthData
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.repository.WidgetRepository
-import de.jensklingenberg.ktorfit.Ktorfit
-import de.jensklingenberg.ktorfit.converter.CallConverterFactory
-import io.ktor.client.HttpClient
-import io.ktor.client.plugins.HttpSend
-import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
-import io.ktor.client.plugins.logging.LogLevel
-import io.ktor.client.plugins.logging.Logging
-import io.ktor.client.plugins.plugin
-import io.ktor.serialization.kotlinx.json.json
-import kotlinx.coroutines.flow.first
-import kotlinx.serialization.json.Json
-
-class WidgetRepositoryProvider(private val dataStore: DataStore) {
- suspend operator fun invoke(): WidgetRepository? {
- val hostSelectionInterceptor = HostSelectionInterceptor()
- val loginData: LoginData? = getAuthData()
- if (loginData == null) {
- return null
- }
- val baseUrl = loginData.baseUrl
- val jwtToken = loginData.accessToken
- if (baseUrl.isBlank() || jwtToken.isBlank()) {
- return null
- }
- hostSelectionInterceptor.setHost(baseUrl.replace("https://", ""))
- hostSelectionInterceptor.setToken(
- jwtToken
- )
-
-
- val json = Json {
- ignoreUnknownKeys = true
- isLenient = true
- explicitNulls = false
- }
-
- val client = HttpClient {
- install(ContentNegotiation) { json(json) }
- install(Logging) {
- logger = object : io.ktor.client.plugins.logging.Logger {
- override fun log(message: String) {
- Logger.v("HttpClient") {
- message.lines().joinToString { "\n\t\t$it" }
- }
- }
- }
- level = LogLevel.BODY
- }
- }.apply {
- plugin(HttpSend).intercept { request ->
- with(hostSelectionInterceptor) {
- intercept(request)
- }
- }
- }
-
- val ktorfit = Ktorfit.Builder()
- .converterFactories(CallConverterFactory())
- .httpClient(client)
- .baseUrl("https://err.or/")
- .build()
-
- val service: PixelfedApi = ktorfit.createPixelfedApi()
- return WidgetRepositoryImpl(service)
- }
-
- private suspend fun getAuthData(): LoginData? {
- val currentlyLoggedIn = dataStore.data.first().currentlyLoggedIn
- return dataStore.data.first().loginDataList.find { it.accountId == currentlyLoggedIn }
- }
-}
\ No newline at end of file
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/latest_image/LatestImageWidget.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/latest_image/LatestImageWidget.kt
index e8ba484a..977dbc2d 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/latest_image/LatestImageWidget.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/latest_image/LatestImageWidget.kt
@@ -11,8 +11,6 @@ import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.action.ActionParameters
-import androidx.glance.action.actionParametersOf
-import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.appwidget.CircularProgressIndicator
import androidx.glance.appwidget.GlanceAppWidget
@@ -32,10 +30,7 @@ import androidx.glance.layout.padding
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
-import com.daniebeler.pfpixelix.MainActivity
import com.daniebeler.pfpixelix.R
-import pixelix.app.generated.resources.Res
-import pixelix.app.generated.resources.*
import com.daniebeler.pfpixelix.widget.WidgetColors
import com.daniebeler.pfpixelix.widget.latest_image.utils.GetImageProvider
import com.daniebeler.pfpixelix.widget.notifications.models.LatestImageStore
@@ -46,12 +41,12 @@ class LatestImageWidget : GlanceAppWidget() {
override var stateDefinition: GlanceStateDefinition<*> = CustomLatestImageStateDefinition
- private val destinationKey = ActionParameters.Key(
- MainActivity.KEY_DESTINATION
- )
- private val destinationKeyParam = ActionParameters.Key(
- MainActivity.KEY_DESTINATION_PARAM
- )
+// private val destinationKey = ActionParameters.Key(
+// MainActivity.KEY_DESTINATION
+// )
+// private val destinationKeyParam = ActionParameters.Key(
+// MainActivity.KEY_DESTINATION_PARAM
+// )
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
@@ -72,14 +67,15 @@ class LatestImageWidget : GlanceAppWidget() {
Image(
provider = GetImageProvider()(state.latestImageUri, context,50f),
contentDescription = "latest home timeline picture",
- modifier = GlanceModifier.fillMaxSize().clickable(
- actionStartActivity(
- actionParametersOf(
- destinationKey to MainActivity.Companion.StartNavigation.Post.toString(),
- destinationKeyParam to state.postId
- )
- )
- )
+ modifier = GlanceModifier.fillMaxSize()
+// .clickable(
+// actionStartActivity(
+// actionParametersOf(
+// destinationKey to MainActivity.Companion.StartNavigation.Post.toString(),
+// destinationKeyParam to state.postId
+// )
+// )
+// )
)
} else {
Column(
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/latest_image/work_manager/LatestImageTask.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/latest_image/work_manager/LatestImageTask.kt
index 3058fafa..c63e4be0 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/latest_image/work_manager/LatestImageTask.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/latest_image/work_manager/LatestImageTask.kt
@@ -1,35 +1,37 @@
package com.daniebeler.pfpixelix.widget.notifications.work_manager
-import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.pm.PackageManager
import androidx.core.content.FileProvider.getUriForFile
-import androidx.datastore.core.DataStore
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import coil3.imageLoader
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import com.daniebeler.pfpixelix.common.Resource
-import com.daniebeler.pfpixelix.domain.model.AuthData
-import com.daniebeler.pfpixelix.widget.WidgetRepositoryProvider
+import com.daniebeler.pfpixelix.domain.repository.WidgetRepository
+import com.daniebeler.pfpixelix.domain.service.session.AuthService
+import com.daniebeler.pfpixelix.utils.KmpContext
import com.daniebeler.pfpixelix.widget.latest_image.updateLatestImageWidget
import com.daniebeler.pfpixelix.widget.latest_image.updateLatestImageWidgetRefreshing
+import com.daniebeler.pfpixelix.widget.notifications.updateNotificationsWidget
+import kotlinx.coroutines.flow.firstOrNull
import me.tatarka.inject.annotations.Inject
class LatestImageTask @Inject constructor(
- private val context: Context,
+ private val context: KmpContext,
workerParams: WorkerParameters,
- private val dataStore: DataStore
+ private val authService: AuthService,
+ private val repository: WidgetRepository,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
try {
updateLatestImageWidgetRefreshing(context)
- val repository = WidgetRepositoryProvider(dataStore).invoke()
- if (repository == null) {
- updateLatestImageWidget("", "", context, "you have to be logged in to an account")
+ authService.openSessionIfExist()
+ if (authService.activeUser.firstOrNull() == null) {
+ updateNotificationsWidget(emptyList(), context, "you have to be logged in to an account")
return Result.failure()
}
val res = repository.getLatestImage()
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/notifications/NotificationsWidget.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/notifications/NotificationsWidget.kt
index 75d508cf..b14b10a4 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/notifications/NotificationsWidget.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/notifications/NotificationsWidget.kt
@@ -14,8 +14,6 @@ import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.LocalSize
import androidx.glance.action.ActionParameters
-import androidx.glance.action.actionParametersOf
-import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.appwidget.CircularProgressIndicator
import androidx.glance.appwidget.GlanceAppWidget
@@ -42,23 +40,20 @@ import androidx.glance.state.GlanceStateDefinition
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
-import com.daniebeler.pfpixelix.MainActivity
import com.daniebeler.pfpixelix.R
-import pixelix.app.generated.resources.Res
-import pixelix.app.generated.resources.*
import com.daniebeler.pfpixelix.widget.WidgetColors
import com.daniebeler.pfpixelix.widget.latest_image.utils.GetImageProvider
import com.daniebeler.pfpixelix.widget.notifications.models.NotificationStoreItem
import com.daniebeler.pfpixelix.widget.notifications.models.NotificationsStore
import com.daniebeler.pfpixelix.widget.notifications.work_manager.NotificationsWorkManager
-private val destinationKey = ActionParameters.Key(
- MainActivity.KEY_DESTINATION
-)
-
-private val destinationKeyParam = ActionParameters.Key(
- MainActivity.KEY_DESTINATION_PARAM
-)
+//private val destinationKey = ActionParameters.Key(
+// MainActivity.KEY_DESTINATION
+//)
+//
+//private val destinationKeyParam = ActionParameters.Key(
+// MainActivity.KEY_DESTINATION_PARAM
+//)
class NotificationsWidget : GlanceAppWidget() {
@@ -106,13 +101,15 @@ class NotificationsWidget : GlanceAppWidget() {
verticalAlignment = Alignment.CenterVertically
) {
Row(
- modifier = GlanceModifier.clickable(
- actionStartActivity(
- actionParametersOf(
- destinationKey to MainActivity.Companion.StartNavigation.Notifications.toString(),
- )
- )
- ), verticalAlignment = Alignment.CenterVertically
+ modifier = GlanceModifier
+// .clickable(
+// actionStartActivity(
+// actionParametersOf(
+// destinationKey to MainActivity.Companion.StartNavigation.Notifications.toString(),
+// )
+// )
+// )
+ , verticalAlignment = Alignment.CenterVertically
) {
if (size.height >= BIG_SQUARE.height && size.width >= BIG_SQUARE.width) {
Image(
@@ -185,14 +182,14 @@ class NotificationsWidget : GlanceAppWidget() {
private fun NotificationItem(notification: NotificationStoreItem, context: Context) {
val size = LocalSize.current
Box(
- modifier = GlanceModifier.clickable(
- actionStartActivity(
- actionParametersOf(
- destinationKey to MainActivity.Companion.StartNavigation.Profile.toString(),
- destinationKeyParam to notification.accountId
- )
- )
- )
+// modifier = GlanceModifier.clickable(
+// actionStartActivity(
+// actionParametersOf(
+// destinationKey to MainActivity.Companion.StartNavigation.Profile.toString(),
+// destinationKeyParam to notification.accountId
+// )
+// )
+// )
) {
Column {
Spacer(GlanceModifier.height(12.dp))
diff --git a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/notifications/work_manager/NotificationsTask.kt b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/notifications/work_manager/NotificationsTask.kt
index ca9c761f..1356a990 100644
--- a/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/notifications/work_manager/NotificationsTask.kt
+++ b/app/src/androidMain/kotlin/com/daniebeler/pfpixelix/widget/notifications/work_manager/NotificationsTask.kt
@@ -1,36 +1,36 @@
package com.daniebeler.pfpixelix.widget.notifications.work_manager
-import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.pm.PackageManager
import androidx.core.content.FileProvider.getUriForFile
-import androidx.datastore.core.DataStore
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import coil3.imageLoader
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import com.daniebeler.pfpixelix.common.Resource
-import com.daniebeler.pfpixelix.domain.model.AuthData
-import com.daniebeler.pfpixelix.widget.WidgetRepositoryProvider
+import com.daniebeler.pfpixelix.domain.repository.WidgetRepository
+import com.daniebeler.pfpixelix.domain.service.session.AuthService
+import com.daniebeler.pfpixelix.utils.KmpContext
import com.daniebeler.pfpixelix.widget.notifications.models.NotificationStoreItem
import com.daniebeler.pfpixelix.widget.notifications.updateNotificationsWidget
import com.daniebeler.pfpixelix.widget.notifications.updateNotificationsWidgetRefreshing
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.firstOrNull
import me.tatarka.inject.annotations.Inject
class NotificationsTask @Inject constructor(
- private val context: Context,
+ private val context: KmpContext,
workerParams: WorkerParameters,
- private val dataStore: DataStore
+ private val authService: AuthService,
+ private val repository: WidgetRepository,
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
try {
updateNotificationsWidgetRefreshing(context)
- val repository = WidgetRepositoryProvider(dataStore).invoke()
- if (repository == null) {
+ authService.openSessionIfExist()
+ if (authService.activeUser.firstOrNull() == null) {
updateNotificationsWidget(emptyList(), context, "you have to be logged in to an account")
return Result.failure()
}
diff --git a/app/src/androidMain/res/values/themes.xml b/app/src/androidMain/res/values/themes.xml
index 668caadb..0b40f6d7 100644
--- a/app/src/androidMain/res/values/themes.xml
+++ b/app/src/androidMain/res/values/themes.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt
new file mode 100644
index 00000000..aee9682a
--- /dev/null
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/App.kt
@@ -0,0 +1,496 @@
+package com.daniebeler.pfpixelix
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.UnfoldMore
+import androidx.compose.material3.DrawerValue
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MaterialTheme.shapes
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.ModalDrawerSheet
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemDefaults
+import androidx.compose.material3.Scaffold
+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.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import androidx.navigation.NavGraphBuilder
+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.common.Destinations
+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.theme.PixelixTheme
+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.painterResource
+import org.jetbrains.compose.resources.stringResource
+import org.jetbrains.compose.resources.vectorResource
+import pixelix.app.generated.resources.Res
+import pixelix.app.generated.resources.default_avatar
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun App(
+ appComponent: AppComponent,
+ exitApp: () -> Unit
+) {
+ val uriHandler = LocalUriHandler.current
+ DisposableEffect(uriHandler) {
+ val systemUrlHandler = appComponent.systemUrlHandler
+ systemUrlHandler.uriHandler = uriHandler
+ onDispose {
+ systemUrlHandler.uriHandler = null
+ }
+ }
+ CompositionLocalProvider(
+ LocalAppComponent provides appComponent
+ ) {
+ PixelixTheme {
+ val navController = rememberNavController()
+ val scope = rememberCoroutineScope()
+ val drawerState = rememberDrawerState(DrawerValue.Closed)
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ var showAccountSwitchBottomSheet by remember { mutableStateOf(false) }
+
+ var activeUser by remember { mutableStateOf("unknown") }
+ LaunchedEffect(Unit) {
+ val authService = appComponent.authService
+ authService.openSessionIfExist()
+ authService.activeUser.collect {
+ activeUser = it
+ }
+ }
+ if (activeUser == "unknown") return@PixelixTheme
+
+ ReverseModalNavigationDrawer(
+ gesturesEnabled = drawerState.isOpen,
+ drawerState = drawerState,
+ drawerContent = {
+ ModalDrawerSheet(
+ drawerState = drawerState,
+ drawerShape = shapes.extraLarge.end(0.dp),
+ ) {
+ PreferencesComposable(navController, drawerState, {
+ scope.launch {
+ drawerState.close()
+ }
+ })
+ }
+ }) {
+
+ Scaffold(
+ bottomBar = {
+ BottomBar(
+ navController = navController,
+ openAccountSwitchBottomSheet = {
+ showAccountSwitchBottomSheet = true
+ },
+ )
+ },
+ content = { paddingValues ->
+ NavHost(
+ modifier = Modifier.fillMaxSize(),
+ navController = navController,
+ startDestination = Destinations.FirstLogin.route,
+ builder = {
+ navigationGraph(
+ navController,
+ { scope.launch { drawerState.open() } },
+ exitApp
+ )
+ }
+ )
+ LaunchedEffect(activeUser) {
+ val rootScreen = if (activeUser == null) {
+ Destinations.FirstLogin.route
+ } else {
+ Destinations.HomeScreen.route
+ }
+ navController.navigate(rootScreen) {
+ val root = navController.currentBackStack.value
+ .firstOrNull { it.destination.route != null }
+ ?.destination?.route
+ if (root != null) {
+ popUpTo(root) { inclusive = true }
+ }
+ }
+
+ if (activeUser != null) {
+ appComponent.systemFileShare.shareFilesRequests.collect { uris ->
+ val urisJson = Json.encodeToString(
+ uris.map { uri -> uri.toString() }
+ )
+ Navigate.navigate(
+ "new_post_screen?uris=$urisJson", navController
+ )
+ }
+ }
+ }
+ }
+ )
+ }
+ if (showAccountSwitchBottomSheet) {
+ ModalBottomSheet(
+ onDismissRequest = {
+ showAccountSwitchBottomSheet = false
+ }, sheetState = sheetState
+ ) {
+ AccountSwitchBottomSheet(
+ navController = navController,
+ closeBottomSheet = { showAccountSwitchBottomSheet = false },
+ null
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun NavGraphBuilder.navigationGraph(
+ navController: NavHostController,
+ openPreferencesDrawer: () -> Unit,
+ exitApp: () -> Unit
+) {
+ dialog(
+ route = Destinations.FirstLogin.route,
+ ) {
+ Dialog(
+ onDismissRequest = exitApp,
+ properties = DialogProperties(
+ dismissOnClickOutside = false
+ )
+ ) {
+ SetUpEdgeToEdgeDialog()
+ LoginComposable()
+ }
+ }
+ dialog(
+ route = Destinations.NewLogin.route,
+ dialogProperties = DialogProperties(
+ dismissOnClickOutside = false
+ )
+ ) {
+ SetUpEdgeToEdgeDialog()
+ LoginComposable()
+ }
+
+ composable(Destinations.HomeScreen.route) {
+ HomeComposable(navController, openPreferencesDrawer)
+ }
+
+ composable(Destinations.NotificationsScreen.route) {
+ NotificationsComposable(navController)
+ }
+
+ composable(Destinations.Profile.route) { navBackStackEntry ->
+ val uId = navBackStackEntry.arguments?.getString("userid")
+
+ uId?.let { id ->
+ OtherProfileComposable(navController, userId = id, byUsername = null)
+
+ }
+ }
+
+ composable(Destinations.ProfileByUsername.route) { navBackStackEntry ->
+ val username = navBackStackEntry.arguments?.getString("username")
+
+ username?.let {
+ OtherProfileComposable(navController, userId = "", byUsername = it)
+ }
+ }
+
+ composable(Destinations.Hashtag.route) { navBackStackEntry ->
+ 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?.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?.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?.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?.getString("postid")
+ val refresh = navBackStackEntry.arguments?.getBoolean("refresh")!!
+ val openReplies = navBackStackEntry.arguments?.getBoolean("openReplies")!!
+ Logger.d("refresh") { refresh.toString() }
+ Logger.d("openReplies") { openReplies.toString() }
+ uId?.let { id ->
+ SinglePostComposable(navController, postId = id, refresh, openReplies)
+ }
+ }
+
+ composable(Destinations.Collection.route) { navBackStackEntry ->
+ val uId = navBackStackEntry.arguments?.getString("collectionid")
+ uId?.let { id ->
+ CollectionComposable(navController, collectionId = id)
+ }
+ }
+
+ composable(Destinations.Search.route) {
+ ExploreComposable(navController)
+ }
+
+ composable(Destinations.Conversation.route) {
+ ConversationsComposable(navController = navController)
+ }
+
+ composable(Destinations.Chat.route) { navBackStackEntry ->
+ val uId = navBackStackEntry.arguments?.getString("userid")
+ uId?.let { id ->
+ ChatComposable(navController = navController, accountId = id)
+ }
+ }
+
+ composable(Destinations.Mention.route) { navBackStackEntry ->
+ val mentionId = navBackStackEntry.arguments?.getString("mentionid")
+ mentionId?.let { id ->
+ MentionComposable(navController = navController, mentionId = id)
+ }
+ }
+}
+
+@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()
+
+ NavigationBar(
+ modifier = Modifier.height(60.dp + systemNavigationBarHeight)
+ ) {
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentRoute = navBackStackEntry?.destination?.route
+ screens.forEach { screen ->
+ val interactionSource = remember { MutableInteractionSource() }
+ val coroutineScope = rememberCoroutineScope()
+ var isLongPress by remember { mutableStateOf(false) }
+
+ LaunchedEffect(interactionSource) {
+ interactionSource.interactions.collect { interaction ->
+ when (interaction) {
+ is PressInteraction.Press -> {
+ isLongPress = false // Reset flag before starting detection
+ coroutineScope.launch {
+ delay(500L) // Long-press threshold
+ if (screen.route == Destinations.OwnProfile.route) {
+ openAccountSwitchBottomSheet()
+ }
+ isLongPress = true
+ }
+ }
+
+ is PressInteraction.Release, is PressInteraction.Cancel -> {
+ coroutineScope.coroutineContext.cancelChildren()
+ }
+ }
+ }
+ }
+ NavigationBarItem(icon = {
+ var avatar by mutableStateOf(null)
+ val appComponent = LocalAppComponent.current
+ LaunchedEffect(Unit) {
+ val authService = appComponent.authService
+ authService.activeUser
+ .map { authService.getCurrentSession() }
+ .collect { avatar = it?.avatar }
+ }
+
+
+ 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)
+ )
+ Icon(
+ Icons.Outlined.UnfoldMore,
+ contentDescription = "long press to switch account"
+ )
+ }
+ } 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,
+ colors = NavigationBarItemDefaults.colors(
+ selectedIconColor = MaterialTheme.colorScheme.inverseSurface,
+ indicatorColor = Color.Transparent
+ ),
+ interactionSource = interactionSource,
+ onClick = {
+ if (!isLongPress) {
+ Navigate.navigateWithPopUp(screen.route, navController)
+ }
+ })
+ }
+ }
+}
+
+//https://partnerissuetracker.corp.google.com/issues/246909281
+@Composable
+expect fun SetUpEdgeToEdgeDialog()
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/common/Destinations.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/common/Destinations.kt
index 121b7dda..11f42b37 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/common/Destinations.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/common/Destinations.kt
@@ -22,6 +22,14 @@ sealed class Destinations(
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,
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/PixelfedApi.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/PixelfedApi.kt
index 4fc51537..bee58a4c 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/PixelfedApi.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/PixelfedApi.kt
@@ -33,6 +33,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.Headers
import de.jensklingenberg.ktorfit.http.POST
import de.jensklingenberg.ktorfit.http.PUT
import de.jensklingenberg.ktorfit.http.Path
@@ -326,8 +327,9 @@ interface PixelfedApi {
@GET("api/v1.1/direct/thread")
fun getChat(@Query("pid") accountId: String, @Query("max_id") maxId: String): Call
+ @Headers("Content-Type: application/json")
@POST("api/v1.1/direct/thread/send")
- fun sendMessage(@Body createMessageDto: CreateMessageDto): Call
+ fun sendMessage(@Body createMessageDto: String): Call
@DELETE("api/v1.1/direct/thread/message")
fun deleteMessage(@Query("id") id: String): Call>
@@ -360,9 +362,10 @@ interface PixelfedApi {
@Query("q") searchText: String
): Call>
+ @Headers("Content-Type: application/json")
@POST("api/v2/media")
fun uploadMedia(
- @Body body: MultiPartFormDataContent
+ @Body body: String
): Call
@FormUrlEncoded
@@ -372,19 +375,22 @@ interface PixelfedApi {
@Field("description") description: String,
): Call
+ @Headers("Content-Type: application/json")
@POST("api/v1/statuses")
suspend fun createPost(
- @Body createPostDto: CreatePostDto
+ @Body createPostDto: String
): Call
+ @Headers("Content-Type: application/json")
@POST("api/v1/statuses")
fun createReply(
- @Body createReplyDto: CreateReplyDto
+ @Body createReplyDto: String
): Call
+ @Headers("Content-Type: application/json")
@PUT("api/v1/statuses/{id}")
suspend fun updatePost(
- @Path("id") postId: String, @Body updatePostDto: UpdatePostDto
+ @Path("id") postId: String, @Body updatePostDto: String
): Call
@DELETE("api/v1/statuses/{id}")
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/dto/CreatePostDto.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/dto/CreatePostDto.kt
index 680e63d5..ce0fef92 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/dto/CreatePostDto.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/remote/dto/CreatePostDto.kt
@@ -1,10 +1,13 @@
package com.daniebeler.pfpixelix.data.remote.dto
-class CreatePostDto(_status: String, _media_ids: List, _sensitive: Boolean, _visibility: String, _spoilerText: String?, _place_id: String?) {
- var status: String = _status
- var media_ids: List = _media_ids
- var sensitive: Boolean = _sensitive
- var spoiler_text: String? = _spoilerText
- var visibility: String? = _visibility
- var place_id: String? = _place_id
-}
\ No newline at end of file
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CreatePostDto(
+ val status: String,
+ val media_ids: List,
+ val sensitive: Boolean,
+ val visibility: String,
+ val spoilerText: String?,
+ val place_id: String?
+)
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/AuthRepositoryImpl.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/AuthRepositoryImpl.kt
index 47e8b256..64ad8bdd 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/AuthRepositoryImpl.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/AuthRepositoryImpl.kt
@@ -1,95 +1,95 @@
-package com.daniebeler.pfpixelix.data.repository
-
-import androidx.datastore.core.DataStore
-import com.daniebeler.pfpixelix.domain.model.AuthData
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
-import kotlinx.coroutines.flow.first
-import me.tatarka.inject.annotations.Inject
-
-class AuthRepositoryImpl @Inject constructor(private val dataStore: DataStore) :
- AuthRepository {
- override suspend fun addNewLoginData(newLoginData: LoginData) {
- try {
- dataStore.updateData { authData ->
- authData.copy(
- loginDataList = authData.loginDataList + newLoginData
- )
- }
- } catch (e: Exception) {
- println(e)
- }
- }
-
- override suspend fun updateOngoingLoginData(
- newLoginData: LoginData
- ) {
- dataStore.updateData { authData ->
- var updatedLoginDataList = authData.loginDataList
-
- updatedLoginDataList = updatedLoginDataList.map { loginData ->
- if (loginData.loginOngoing) {
- newLoginData
- } else {
- loginData
- }
- }
-
- authData.copy(
- loginDataList = updatedLoginDataList
- )
- }
- }
-
- override suspend fun finishLogin(
- newLoginData: LoginData, currentlyLoggedIn: String
- ) {
- dataStore.updateData { authData ->
- var updatedLoginDataList = authData.loginDataList
- updatedLoginDataList =
- updatedLoginDataList.filter { it.accountId != newLoginData.accountId && it.accountId.isNotBlank() && !it.loginOngoing }
-
-
- updatedLoginDataList = updatedLoginDataList + newLoginData
-
- authData.copy(
- loginDataList = updatedLoginDataList, currentlyLoggedIn = currentlyLoggedIn
- )
- }
- }
-
- override suspend fun updateCurrentUser(accountId: String) {
- dataStore.updateData { authData ->
- if (authData.loginDataList.any { it.accountId == accountId }) {
- authData.copy(currentlyLoggedIn = accountId)
- } else {
- authData
- }
- }
- }
-
- override suspend fun getAuthData(): AuthData {
- return dataStore.data.first()
- }
-
- override suspend fun getOngoingLogin(): LoginData? {
- return dataStore.data.first().loginDataList.find { it.loginOngoing }
- }
-
- override suspend fun getCurrentLoginData(): LoginData? {
- val currentlyLoggedIn = dataStore.data.first().currentlyLoggedIn
- return dataStore.data.first().loginDataList.find { it.accountId == currentlyLoggedIn }
- }
-
- override suspend fun logout() {
- dataStore.updateData { authData ->
- authData.copy(loginDataList = authData.loginDataList.filter { loginData -> loginData.accountId != authData.currentlyLoggedIn }, currentlyLoggedIn = "")
- }
- }
-
- override suspend fun removeLoginData(accountId: String) {
- dataStore.updateData { authData ->
- authData.copy(loginDataList = authData.loginDataList.filter { loginData -> loginData.accountId != accountId })
- }
- }
-}
\ No newline at end of file
+//package com.daniebeler.pfpixelix.data.repository
+//
+//import androidx.datastore.core.DataStore
+//import com.daniebeler.pfpixelix.domain.model.AuthData
+//import com.daniebeler.pfpixelix.domain.model.LoginData
+//import com.daniebeler.pfpixelix.domain.repository.AuthRepository
+//import kotlinx.coroutines.flow.first
+//import me.tatarka.inject.annotations.Inject
+//
+//class AuthRepositoryImpl @Inject constructor(private val dataStore: DataStore) :
+// AuthRepository {
+// override suspend fun addNewLoginData(newLoginData: LoginData) {
+// try {
+// dataStore.updateData { authData ->
+// authData.copy(
+// loginDataList = authData.loginDataList + newLoginData
+// )
+// }
+// } catch (e: Exception) {
+// println(e)
+// }
+// }
+//
+// override suspend fun updateOngoingLoginData(
+// newLoginData: LoginData
+// ) {
+// dataStore.updateData { authData ->
+// var updatedLoginDataList = authData.loginDataList
+//
+// updatedLoginDataList = updatedLoginDataList.map { loginData ->
+// if (loginData.loginOngoing) {
+// newLoginData
+// } else {
+// loginData
+// }
+// }
+//
+// authData.copy(
+// loginDataList = updatedLoginDataList
+// )
+// }
+// }
+//
+// override suspend fun finishLogin(
+// newLoginData: LoginData, currentlyLoggedIn: String
+// ) {
+// dataStore.updateData { authData ->
+// var updatedLoginDataList = authData.loginDataList
+// updatedLoginDataList =
+// updatedLoginDataList.filter { it.accountId != newLoginData.accountId && it.accountId.isNotBlank() && !it.loginOngoing }
+//
+//
+// updatedLoginDataList = updatedLoginDataList + newLoginData
+//
+// authData.copy(
+// loginDataList = updatedLoginDataList, currentlyLoggedIn = currentlyLoggedIn
+// )
+// }
+// }
+//
+// override suspend fun updateCurrentUser(accountId: String) {
+// dataStore.updateData { authData ->
+// if (authData.loginDataList.any { it.accountId == accountId }) {
+// authData.copy(currentlyLoggedIn = accountId)
+// } else {
+// authData
+// }
+// }
+// }
+//
+// override suspend fun getAuthData(): AuthData {
+// return dataStore.data.first()
+// }
+//
+// override suspend fun getOngoingLogin(): LoginData? {
+// return dataStore.data.first().loginDataList.find { it.loginOngoing }
+// }
+//
+// override suspend fun getCurrentLoginData(): LoginData? {
+// val currentlyLoggedIn = dataStore.data.first().currentlyLoggedIn
+// return dataStore.data.first().loginDataList.find { it.accountId == currentlyLoggedIn }
+// }
+//
+// override suspend fun logout() {
+// dataStore.updateData { authData ->
+// authData.copy(loginDataList = authData.loginDataList.filter { loginData -> loginData.accountId != authData.currentlyLoggedIn }, currentlyLoggedIn = "")
+// }
+// }
+//
+// override suspend fun removeLoginData(accountId: String) {
+// dataStore.updateData { authData ->
+// authData.copy(loginDataList = authData.loginDataList.filter { loginData -> loginData.accountId != accountId })
+// }
+// }
+//}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/CountryRepositoryImpl.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/CountryRepositoryImpl.kt
index 68c7100f..f6dd94ae 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/CountryRepositoryImpl.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/CountryRepositoryImpl.kt
@@ -39,12 +39,14 @@ import com.daniebeler.pfpixelix.utils.execute
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
+import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject
class CountryRepositoryImpl @Inject constructor(
private val userDataStorePreferences: DataStore,
- private val pixelfedApi: PixelfedApi
+ private val pixelfedApi: PixelfedApi,
+ private val json: Json
) : CountryRepository {
override fun getAuthV1Token(): Flow = userDataStorePreferences.data.map { preferences ->
preferences[stringPreferencesKey(Constants.ACCESS_TOKEN_DATASTORE_KEY)] ?: ""
@@ -128,7 +130,9 @@ class CountryRepositoryImpl @Inject constructor(
override fun createReply(postId: String, content: String): Flow> {
val dto = CreateReplyDto(status = content, in_reply_to_id = postId)
- return NetworkCall().makeCall(pixelfedApi.createReply(dto))
+ return NetworkCall().makeCall(
+ pixelfedApi.createReply(json.encodeToString(dto))
+ )
}
// Auth
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/DirectMessagesRepositoryImpl.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/DirectMessagesRepositoryImpl.kt
index d0c9bf67..a8dc6a5a 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/DirectMessagesRepositoryImpl.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/DirectMessagesRepositoryImpl.kt
@@ -14,10 +14,12 @@ import com.daniebeler.pfpixelix.utils.NetworkCall
import com.daniebeler.pfpixelix.utils.execute
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
+import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject
class DirectMessagesRepositoryImpl @Inject constructor(
- private val pixelfedApi: PixelfedApi
+ private val pixelfedApi: PixelfedApi,
+ private val json: Json
) : DirectMessagesRepository {
override fun getConversations(): Flow>> {
@@ -33,7 +35,9 @@ class DirectMessagesRepositoryImpl @Inject constructor(
}
override fun sendMessage(createMessageDto: CreateMessageDto): Flow> {
- return NetworkCall().makeCall(pixelfedApi.sendMessage(createMessageDto))
+ return NetworkCall().makeCall(
+ pixelfedApi.sendMessage(json.encodeToString(createMessageDto))
+ )
}
override fun deleteMessage(id: String): Flow>> = flow {
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/PostEditorRepositoryImpl.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/PostEditorRepositoryImpl.kt
index a18ffb14..5ec7c766 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/PostEditorRepositoryImpl.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/PostEditorRepositoryImpl.kt
@@ -1,5 +1,6 @@
package com.daniebeler.pfpixelix.data.repository
+import androidx.compose.foundation.content.MediaType
import com.daniebeler.pfpixelix.common.Resource
import com.daniebeler.pfpixelix.data.remote.PixelfedApi
import com.daniebeler.pfpixelix.data.remote.dto.CreatePostDto
@@ -15,15 +16,20 @@ import com.daniebeler.pfpixelix.utils.KmpUri
import com.daniebeler.pfpixelix.utils.NetworkCall
import com.daniebeler.pfpixelix.utils.execute
import io.ktor.client.request.forms.MultiPartFormDataContent
+import io.ktor.client.request.forms.append
import io.ktor.client.request.forms.formData
+import io.ktor.http.BadContentTypeFormatException
+import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
+import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject
class PostEditorRepositoryImpl @Inject constructor(
- private val pixelfedApi: PixelfedApi
+ private val pixelfedApi: PixelfedApi,
+ private val json: Json
) : PostEditorRepository {
override fun uploadMedia(
@@ -42,20 +48,23 @@ class PostEditorRepositoryImpl @Inject constructor(
val bytes = file.getBytes()
val thumbnail = file.getThumbnail()
- val data = MultiPartFormDataContent(formData {
- append("file", bytes, Headers.build {
- append(HttpHeaders.ContentType, file.getMimeType())
- append(HttpHeaders.ContentDisposition, file.getName())
- })
- if (thumbnail != null) {
- append("thumbnail", thumbnail, Headers.build {
- append(HttpHeaders.ContentDisposition, "thumbnail")
+ val data = MultiPartFormDataContent(
+ parts = formData {
+ append("description", description)
+ append("file", bytes, Headers.build {
+ append(HttpHeaders.ContentType, file.getMimeType())
+ append(HttpHeaders.ContentDisposition, "filename=${file.getName()}")
})
+ if (thumbnail != null) {
+ append("thumbnail", thumbnail, Headers.build {
+ append(HttpHeaders.ContentDisposition, "filename=thumbnail")
+ })
+ }
}
- })
+ )
try {
- val res = pixelfedApi.uploadMedia(data).execute().toModel()
+ val res = pixelfedApi.uploadMedia(json.encodeToString(data)).execute().toModel()
emit(Resource.Success(res))
} catch (e: Exception) {
emit(Resource.Error("Unknown Error"))
@@ -76,7 +85,7 @@ class PostEditorRepositoryImpl @Inject constructor(
override fun createPost(createPostDto: CreatePostDto): Flow> = flow {
try {
emit(Resource.Loading())
- val res = pixelfedApi.createPost(createPostDto).execute().toModel()
+ val res = pixelfedApi.createPost(json.encodeToString(createPostDto)).execute().toModel()
emit(Resource.Success(res))
} catch (exception: Exception) {
if (exception.message != null) {
@@ -91,7 +100,7 @@ class PostEditorRepositoryImpl @Inject constructor(
flow {
try {
emit(Resource.Loading())
- pixelfedApi.updatePost(postId, updatePostDto).execute()
+ pixelfedApi.updatePost(postId, json.encodeToString(updatePostDto)).execute()
emit(Resource.Success(null))
} catch (exception: Exception) {
if (exception.message != null) {
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/WidgetRepositoryImpl.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/WidgetRepositoryImpl.kt
index a4b11ee6..0f17b262 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/WidgetRepositoryImpl.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/data/repository/WidgetRepositoryImpl.kt
@@ -5,8 +5,12 @@ import com.daniebeler.pfpixelix.data.remote.PixelfedApi
import com.daniebeler.pfpixelix.domain.model.Post
import com.daniebeler.pfpixelix.domain.repository.WidgetRepository
import com.daniebeler.pfpixelix.utils.execute
+import me.tatarka.inject.annotations.Inject
-class WidgetRepositoryImpl constructor(private val pixelfedApi: PixelfedApi): WidgetRepository{
+@Inject
+class WidgetRepositoryImpl(
+ private val pixelfedApi: PixelfedApi
+): WidgetRepository {
override suspend fun getNotifications(): Resource> {
try {
val response = pixelfedApi.getNotifications().execute()
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 0ec63dd7..fe676253 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/AppComponent.kt
@@ -1,6 +1,5 @@
package com.daniebeler.pfpixelix.di
-import HostSelectionInterceptor
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.okio.OkioStorage
@@ -14,7 +13,6 @@ import coil3.request.CachePolicy
import com.daniebeler.pfpixelix.data.remote.PixelfedApi
import com.daniebeler.pfpixelix.data.remote.createPixelfedApi
import com.daniebeler.pfpixelix.data.repository.AccountRepositoryImpl
-import com.daniebeler.pfpixelix.data.repository.AuthRepositoryImpl
import com.daniebeler.pfpixelix.data.repository.CollectionRepositoryImpl
import com.daniebeler.pfpixelix.data.repository.CountryRepositoryImpl
import com.daniebeler.pfpixelix.data.repository.DirectMessagesRepositoryImpl
@@ -25,10 +23,8 @@ import com.daniebeler.pfpixelix.data.repository.SavedSearchesRepositoryImpl
import com.daniebeler.pfpixelix.data.repository.StorageRepositoryImpl
import com.daniebeler.pfpixelix.data.repository.TimelineRepositoryImpl
import com.daniebeler.pfpixelix.data.repository.WidgetRepositoryImpl
-import com.daniebeler.pfpixelix.domain.model.AuthData
import com.daniebeler.pfpixelix.domain.model.SavedSearches
import com.daniebeler.pfpixelix.domain.repository.AccountRepository
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
import com.daniebeler.pfpixelix.domain.repository.CollectionRepository
import com.daniebeler.pfpixelix.domain.repository.CountryRepository
import com.daniebeler.pfpixelix.domain.repository.DirectMessagesRepository
@@ -39,7 +35,12 @@ import com.daniebeler.pfpixelix.domain.repository.SavedSearchesRepository
import com.daniebeler.pfpixelix.domain.repository.StorageRepository
import com.daniebeler.pfpixelix.domain.repository.TimelineRepository
import com.daniebeler.pfpixelix.domain.repository.WidgetRepository
-import com.daniebeler.pfpixelix.utils.AuthDataSerializer
+import com.daniebeler.pfpixelix.domain.service.session.AuthService
+import com.daniebeler.pfpixelix.domain.service.session.Session
+import com.daniebeler.pfpixelix.domain.service.session.SessionStorage
+import com.daniebeler.pfpixelix.domain.service.session.SessionStorageDataSerializer
+import com.daniebeler.pfpixelix.domain.service.session.SystemUrlHandler
+import com.daniebeler.pfpixelix.domain.service.share.SystemFileShare
import com.daniebeler.pfpixelix.utils.KmpContext
import com.daniebeler.pfpixelix.utils.SavedSearchesSerializer
import com.daniebeler.pfpixelix.utils.coilContext
@@ -49,6 +50,7 @@ import de.jensklingenberg.ktorfit.Ktorfit
import de.jensklingenberg.ktorfit.converter.CallConverterFactory
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpSend
+import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logging
@@ -56,6 +58,7 @@ import io.ktor.client.plugins.plugin
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.KmpComponentCreate
import me.tatarka.inject.annotations.Provides
import me.tatarka.inject.annotations.Scope
import okio.FileSystem
@@ -70,26 +73,23 @@ annotation class AppSingleton
abstract class AppComponent(
@get:Provides val context: KmpContext
) {
-
- @Provides
- @AppSingleton
- fun provideJson(): Json = Json {
+ abstract val systemUrlHandler: SystemUrlHandler
+ abstract val systemFileShare: SystemFileShare
+ abstract val authService: AuthService
+
+ @get:Provides
+ @get:AppSingleton
+ val json = Json {
ignoreUnknownKeys = true
isLenient = true
explicitNulls = false
}
- @Provides
- @AppSingleton
- fun provideHostSelectionInterceptor(): HostSelectionInterceptorInterface =
- HostSelectionInterceptor()
-
-
@Provides
@AppSingleton
fun provideHttpClient(
json: Json,
- hostSelectionInterceptor: HostSelectionInterceptorInterface
+ session: Session
): HttpClient = HttpClient {
install(ContentNegotiation) { json(json) }
install(Logging) {
@@ -102,28 +102,26 @@ abstract class AppComponent(
}
level = LogLevel.BODY
}
+ install(HttpTimeout) {
+ requestTimeoutMillis = 60000
+ socketTimeoutMillis = 60000
+ connectTimeoutMillis = 60000
+ }
}.apply {
plugin(HttpSend).intercept { request ->
- with(hostSelectionInterceptor) {
- intercept(request)
- }
+ with(session) { intercept(request) }
}
}
-
@Provides
@AppSingleton
- fun provideKtorfit(client: HttpClient): Ktorfit = Ktorfit.Builder()
- .converterFactories(CallConverterFactory())
- .httpClient(client)
- .baseUrl("https://err.or/")
- .build()
-
-
- @Provides
- @AppSingleton
- fun providePixelfedApi(ktorfit: Ktorfit): PixelfedApi =
- ktorfit.createPixelfedApi()
+ fun providePixelfedApi(client: HttpClient): PixelfedApi =
+ Ktorfit.Builder()
+ .converterFactories(CallConverterFactory())
+ .httpClient(client)
+ .baseUrl("https://err.or/")
+ .build()
+ .createPixelfedApi()
@Provides
@AppSingleton
@@ -147,12 +145,12 @@ abstract class AppComponent(
@Provides
@AppSingleton
- fun provideAuthDataDataStore(context: KmpContext): DataStore =
+ fun provideSessionStorageDataStore(context: KmpContext): DataStore =
DataStoreFactory.create(
storage = OkioStorage(
fileSystem = FileSystem.SYSTEM,
- producePath = { context.dataStoreDir.resolve("auth_data_datastore.json") },
- serializer = AuthDataSerializer,
+ producePath = { context.dataStoreDir.resolve("session_storage_datastore.json") },
+ serializer = SessionStorageDataSerializer,
)
)
@@ -174,12 +172,10 @@ abstract class AppComponent(
.build()
)
.build()
-
+
@Provides
fun provideAccountRepository(impl: AccountRepositoryImpl): AccountRepository = impl
@Provides
- fun provideAuthRepository(impl: AuthRepositoryImpl): AuthRepository = impl
- @Provides
fun provideCollectionRepository(impl: CollectionRepositoryImpl): CollectionRepository = impl
@Provides
fun provideCountryRepository(impl: CountryRepositoryImpl): CountryRepository = impl
@@ -199,4 +195,9 @@ abstract class AppComponent(
fun provideTimelineRepository(impl: TimelineRepositoryImpl): TimelineRepository = impl
@Provides
fun provideWidgetRepository(impl: WidgetRepositoryImpl): WidgetRepository = impl
-}
\ No newline at end of file
+
+ companion object
+}
+
+@KmpComponentCreate
+expect fun AppComponent.Companion.create(context: KmpContext): AppComponent
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/EntryPointComponent.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/EntryPointComponent.kt
deleted file mode 100644
index b3fcf35e..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/EntryPointComponent.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.daniebeler.pfpixelix.di
-
-import com.daniebeler.pfpixelix.domain.repository.CountryRepository
-import com.daniebeler.pfpixelix.domain.usecase.AddNewLoginUseCase
-import com.daniebeler.pfpixelix.domain.usecase.FinishLoginUseCase
-import com.daniebeler.pfpixelix.domain.usecase.GetCurrentLoginDataUseCase
-import com.daniebeler.pfpixelix.domain.usecase.GetOngoingLoginUseCase
-import com.daniebeler.pfpixelix.domain.usecase.ObtainTokenUseCase
-import com.daniebeler.pfpixelix.domain.usecase.UpdateLoginDataUseCase
-import com.daniebeler.pfpixelix.domain.usecase.VerifyTokenUseCase
-import me.tatarka.inject.annotations.Component
-
-@Component
-abstract class EntryPointComponent(
- @Component val appComponent: AppComponent
-) {
- abstract val obtainTokenUseCase: ObtainTokenUseCase
- abstract val verifyTokenUseCase: VerifyTokenUseCase
- abstract val updateLoginDataUseCase: UpdateLoginDataUseCase
- abstract val finishLoginUseCase: FinishLoginUseCase
- abstract val newLoginDataUseCase: AddNewLoginUseCase
- abstract val getOngoingLoginUseCase: GetOngoingLoginUseCase
- abstract val hostSelectionInterceptorInterface: HostSelectionInterceptorInterface
- abstract val currentLoginDataUseCase: GetCurrentLoginDataUseCase
- abstract val repository: CountryRepository
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/HostSelectionInterceptor.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/HostSelectionInterceptor.kt
deleted file mode 100644
index f8277abc..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/HostSelectionInterceptor.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-import com.daniebeler.pfpixelix.di.HostSelectionInterceptorInterface
-import io.ktor.client.call.HttpClientCall
-import io.ktor.client.plugins.Sender
-import io.ktor.client.request.HttpRequestBuilder
-import io.ktor.http.set
-import kotlin.concurrent.Volatile
-
-/** An interceptor that allows runtime changes to the URL hostname. */
-class HostSelectionInterceptor : HostSelectionInterceptorInterface {
- @Volatile
- private var host: String? = null
-
- @Volatile
- private var token: String? = null
- override fun setHost(host: String?) {
- this.host = host
- }
-
- override fun setToken(token: String?) {
- this.token = token
- }
-
- override suspend fun Sender.intercept(request: HttpRequestBuilder): HttpClientCall {
- if (request.url.toString().startsWith("https://err.or")) {
- request.apply {
- url.set(host = host)
- headers["Authorization"] = "Bearer $token"
- }
- }
- return execute(request)
- }
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/HostSelectionInterceptorInterface.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/HostSelectionInterceptorInterface.kt
deleted file mode 100644
index 0d10b50d..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/HostSelectionInterceptorInterface.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.daniebeler.pfpixelix.di
-
-import io.ktor.client.call.HttpClientCall
-import io.ktor.client.plugins.Sender
-import io.ktor.client.request.HttpRequestBuilder
-
-interface HostSelectionInterceptorInterface {
-
- fun setHost(host: String?)
- fun setToken(token: String?)
-
- suspend fun Sender.intercept(request: HttpRequestBuilder): HttpClientCall
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/ViewModelComponent.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/ViewModelComponent.kt
index 452c4870..22542633 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/ViewModelComponent.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/di/ViewModelComponent.kt
@@ -3,7 +3,6 @@ package com.daniebeler.pfpixelix.di
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
-import com.daniebeler.pfpixelix.ui.composables.LoginViewModel
import com.daniebeler.pfpixelix.ui.composables.collection.CollectionViewModel
import com.daniebeler.pfpixelix.ui.composables.custom_account.CustomAccountViewModel
import com.daniebeler.pfpixelix.ui.composables.direct_messages.chat.ChatViewModel
@@ -28,6 +27,7 @@ import com.daniebeler.pfpixelix.ui.composables.profile.other_profile.OtherProfil
import com.daniebeler.pfpixelix.ui.composables.profile.own_profile.AccountSwitchViewModel
import com.daniebeler.pfpixelix.ui.composables.profile.own_profile.OwnProfileViewModel
import com.daniebeler.pfpixelix.ui.composables.profile.server_stats.ServerStatsViewModel
+import com.daniebeler.pfpixelix.ui.composables.session.LoginViewModel
import com.daniebeler.pfpixelix.ui.composables.settings.about_instance.AboutInstanceViewModel
import com.daniebeler.pfpixelix.ui.composables.settings.about_pixelix.AboutPixelixViewModel
import com.daniebeler.pfpixelix.ui.composables.settings.blocked_accounts.BlockedAccountsViewModel
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/AuthData.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/AuthData.kt
index 3bd45672..162c86b9 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/AuthData.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/model/AuthData.kt
@@ -1,6 +1,7 @@
package com.daniebeler.pfpixelix.domain.model
import com.daniebeler.pfpixelix.common.Constants
+import com.daniebeler.pfpixelix.domain.service.session.Credentials
import kotlinx.serialization.Serializable
@Serializable
@@ -23,14 +24,14 @@ data class LoginData(
val loginOngoing: Boolean = false,
)
-fun loginDataToAccount(loginData: LoginData): Account {
+fun credentialsToAccount(credentials: Credentials): Account {
return Account(
- username = loginData.username,
- avatar = loginData.avatar,
- url = loginData.baseUrl,
- id = loginData.accountId,
- displayname = loginData.displayName,
- followersCount = loginData.followers,
+ username = credentials.username,
+ avatar = credentials.avatar,
+ url = credentials.serverUrl,
+ id = credentials.accountId,
+ displayname = credentials.displayName,
+ followersCount = 0,
acct = "",
note = "",
locked = false,
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
new file mode 100644
index 00000000..4379c39a
--- /dev/null
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthApi.kt
@@ -0,0 +1,49 @@
+package com.daniebeler.pfpixelix.domain.service.session
+
+import com.daniebeler.pfpixelix.data.remote.dto.AccountDto
+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.POST
+import de.jensklingenberg.ktorfit.http.Query
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+interface AuthApi {
+ @POST("api/v1/apps")
+ suspend fun getAuthData(
+ @Query("client_name") clientName: String,
+ @Query("redirect_uris") redirectUrl: String
+ ): AuthData
+
+ @FormUrlEncoded
+ @POST("oauth/token")
+ suspend fun getToken(
+ @Field("client_id") clientId: String,
+ @Field("client_secret") clientSecret: String,
+ @Field("code") code: String,
+ @Field("redirect_uri") redirectUri: String,
+ @Field("grant_type") grantType: String
+ ): AuthToken
+
+ @GET("api/v1/accounts/verify_credentials")
+ suspend fun verify(
+ @Header("Authorization") token: String
+ ): AccountDto
+}
+
+@Serializable
+data class AuthData(
+ @SerialName("name") val name: String,
+ @SerialName("id") val id: String,
+ @SerialName("redirect_uri") val redirectUri: String,
+ @SerialName("client_id") val clientId: String,
+ @SerialName("client_secret") val clientSecret: String
+)
+
+@Serializable
+data class AuthToken(
+ @SerialName("access_token") val accessToken: String,
+ @SerialName("created_at") val createdAt: String
+)
\ No newline at end of file
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
new file mode 100644
index 00000000..d1642983
--- /dev/null
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/AuthService.kt
@@ -0,0 +1,151 @@
+package com.daniebeler.pfpixelix.domain.service.session
+
+import androidx.datastore.core.DataStore
+import co.touchlab.kermit.Logger
+import com.daniebeler.pfpixelix.di.AppSingleton
+import de.jensklingenberg.ktorfit.Ktorfit
+import io.ktor.client.HttpClient
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.http.URLBuilder
+import io.ktor.http.Url
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.serialization.json.Json
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+@AppSingleton
+class AuthService(
+ private val urlHandler: SystemUrlHandler,
+ private val session: Session,
+ private val sessionStorage: DataStore,
+ private val json: Json
+) {
+ companion object {
+ private const val clientName = "pixelix"
+ private const val grantType = "authorization_code"
+ private const val redirectUrl = "pixelix-android-auth://callback"
+ private val domainRegex: Regex =
+ "^((\\*)|((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|((\\*\\.)?([a-zA-Z0-9-]+\\.){0,5}[a-zA-Z0-9-][a-zA-Z0-9-]+\\.[a-zA-Z]{2,63}?))\$".toRegex()
+ }
+
+ val activeUser: Flow = session.credentials.map { it?.accountId }
+
+ suspend fun auth(host: String) {
+ Logger.d("new authorization")
+ val serverUrl = getServerUrl(host)
+ val api = createAuthApi(serverUrl)
+ val authData = api.getAuthData(clientName, redirectUrl)
+
+ val authUrl = URLBuilder("${serverUrl}oauth/authorize").apply {
+ parameters.apply {
+ append("response_type", "code")
+ append("redirect_uri", redirectUrl)
+ append("client_id", authData.clientId)
+ }
+ }.build()
+
+ Logger.d("open oauth url")
+ urlHandler.openBrowser(authUrl.toString())
+
+ Logger.d("wait oauth redirect")
+ val redirect = Url(urlHandler.redirects.first())
+ Logger.d("handle oauth redirect")
+
+ val code = redirect.parameters["code"] ?: error("Redirect doesn't have a code")
+
+ val token = api.getToken(
+ authData.clientId,
+ authData.clientSecret,
+ code,
+ redirectUrl,
+ grantType
+ )
+
+ val account = api.verify("Bearer ${token.accessToken}")
+
+ val newCred = Credentials(
+ accountId = requireNotNull(account.id),
+ username = requireNotNull(account.username),
+ displayName = account.displayName ?: account.username,
+ avatar = account.avatar.orEmpty(),
+ serverUrl = serverUrl.toString(),
+ token = token.accessToken
+ )
+ sessionStorage.updateData { data ->
+ data.copy(
+ sessions = data.sessions + newCred,
+ activeUserId = newCred.accountId
+ )
+ }
+ session.setCredentials(newCred)
+ }
+
+ suspend fun openSessionIfExist(userId: String? = null) {
+ sessionStorage.updateData { data ->
+ val cred = if (userId == null) {
+ data.getActiveSession()
+ } else {
+ data.sessions.firstOrNull { it.accountId == userId }
+ }
+ session.setCredentials(cred)
+ data.copy(activeUserId = cred?.accountId)
+ }
+ }
+
+ fun isValidHost(host: String): Boolean = domainRegex.matches(host)
+
+ suspend fun deleteSession(userId: String? = null) {
+ sessionStorage.updateData { data ->
+ val id = userId ?: data.activeUserId
+ val newSessions = data.sessions.filter { it.accountId != id }.toSet()
+ val newId = if (data.activeUserId == id) {
+ newSessions.firstOrNull()?.accountId
+ } else {
+ data.activeUserId
+ }
+ data.copy(sessions = newSessions, activeUserId = newId)
+ }
+ if (userId == null) {
+ openSessionIfExist()
+ }
+ }
+
+ suspend fun getAvailableSessions(): SessionStorage {
+ return sessionStorage.data.first()
+ }
+
+ fun getCurrentSession(): Credentials? {
+ return session.credentials.value
+ }
+
+ private fun getServerUrl(host: String): Url {
+ require(isValidHost(host)) { "The host is invalid '$host'" }
+ return Url("https://$host/")
+ }
+
+ private fun createAuthApi(baseUrl: Url): AuthApi {
+ val httpClient = HttpClient {
+ install(ContentNegotiation) { json(json) }
+ install(Logging) {
+ logger = object : io.ktor.client.plugins.logging.Logger {
+ override fun log(message: String) {
+ Logger.v("HttpAuth") {
+ message.lines().joinToString { "\n\t\t$it" }
+ }
+ }
+ }
+ level = LogLevel.INFO
+ }
+ }
+ val ktorfit = Ktorfit.Builder()
+ .httpClient(httpClient)
+ .baseUrl(baseUrl.toString())
+ .build()
+ return ktorfit.createAuthApi()
+ }
+}
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/Credentials.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/Credentials.kt
new file mode 100644
index 00000000..f4620b34
--- /dev/null
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/Credentials.kt
@@ -0,0 +1,54 @@
+package com.daniebeler.pfpixelix.domain.service.session
+
+import androidx.datastore.core.okio.OkioSerializer
+import com.daniebeler.pfpixelix.domain.model.AuthData
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.builtins.ListSerializer
+import kotlinx.serialization.json.Json
+import okio.BufferedSink
+import okio.BufferedSource
+
+@Serializable
+data class Credentials(
+ val accountId: String,
+ val username: String,
+ val displayName: String,
+ val avatar: String,
+ val serverUrl: String,
+ val token: String
+)
+
+@Serializable
+data class SessionStorage(
+ val sessions: Set,
+ val activeUserId: String?
+) {
+ fun getActiveSession() = activeUserId?.let { sessions.first { s -> s.accountId == it }}
+}
+
+object SessionStorageDataSerializer: OkioSerializer {
+ override val defaultValue: SessionStorage
+ get() = SessionStorage(emptySet(), null)
+
+ override suspend fun readFrom(source: BufferedSource): SessionStorage {
+ return try {
+ Json.decodeFromString(
+ deserializer = SessionStorage.serializer(),
+ string = source.readUtf8()
+ )
+ } catch (e: SerializationException) {
+ e.printStackTrace();
+ defaultValue
+ }
+ }
+
+ override suspend fun writeTo(t: SessionStorage, sink: BufferedSink) {
+ sink.write(
+ Json.encodeToString(
+ serializer = SessionStorage.serializer(),
+ value = t
+ ).encodeToByteArray()
+ )
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..37b69cae
--- /dev/null
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/Session.kt
@@ -0,0 +1,36 @@
+package com.daniebeler.pfpixelix.domain.service.session
+
+import com.daniebeler.pfpixelix.di.AppSingleton
+import io.ktor.client.call.HttpClientCall
+import io.ktor.client.plugins.Sender
+import io.ktor.client.request.HttpRequestBuilder
+import io.ktor.http.Url
+import io.ktor.http.set
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+@AppSingleton
+class Session {
+ private val credentialsState = MutableStateFlow(null)
+ val credentials: StateFlow = credentialsState.asStateFlow()
+
+ fun setCredentials(credentials: Credentials?) {
+ credentialsState.value = credentials
+ }
+
+ 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}"
+ }
+ }
+ return execute(request)
+ }
+}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/SystemUrlHandler.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/SystemUrlHandler.kt
new file mode 100644
index 00000000..daf3bd3b
--- /dev/null
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/session/SystemUrlHandler.kt
@@ -0,0 +1,28 @@
+package com.daniebeler.pfpixelix.domain.service.session
+
+import androidx.compose.ui.platform.UriHandler
+import com.daniebeler.pfpixelix.di.AppSingleton
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+@AppSingleton
+class SystemUrlHandler {
+ private val redirectsFlow = MutableSharedFlow()
+ val redirects = redirectsFlow.asSharedFlow()
+
+ var uriHandler: UriHandler? = null
+
+ fun openBrowser(url: String) {
+ uriHandler?.openUri(url)
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ fun onRedirect(url: String) {
+ GlobalScope.launch { redirectsFlow.emit(url) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/share/SystemFileShare.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/share/SystemFileShare.kt
new file mode 100644
index 00000000..bb141511
--- /dev/null
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/service/share/SystemFileShare.kt
@@ -0,0 +1,28 @@
+package com.daniebeler.pfpixelix.domain.service.share
+
+import com.daniebeler.pfpixelix.di.AppSingleton
+import com.daniebeler.pfpixelix.utils.KmpUri
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+@AppSingleton
+class SystemFileShare {
+ private val shareFilesRequestQueue = Channel>(
+ capacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val shareFilesRequests get() = shareFilesRequestQueue.receiveAsFlow()
+
+ @OptIn(DelicateCoroutinesApi::class)
+ fun share(uris: List) {
+ GlobalScope.launch {
+ shareFilesRequestQueue.send(uris)
+ }
+ }
+}
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/AddNewLoginUseCase.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/AddNewLoginUseCase.kt
deleted file mode 100644
index 4c7bdc13..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/AddNewLoginUseCase.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.daniebeler.pfpixelix.domain.usecase
-
-import com.daniebeler.pfpixelix.di.HostSelectionInterceptorInterface
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
-import me.tatarka.inject.annotations.Inject
-
-@Inject
-class AddNewLoginUseCase(
- private val authRepository: AuthRepository,
- private val hostSelectionInterceptorInterface: HostSelectionInterceptorInterface
-) {
- suspend operator fun invoke(loginData: LoginData) {
- if (loginData.baseUrl.isNotBlank()) {
- hostSelectionInterceptorInterface.setHost(loginData.baseUrl.replace("https://", "")) }
- if (loginData.accessToken.isNotBlank()) {
- hostSelectionInterceptorInterface.setToken(loginData.accessToken)
- }
- authRepository.addNewLoginData(loginData)
- }
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/FinishLoginUseCase.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/FinishLoginUseCase.kt
deleted file mode 100644
index 98dcbe46..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/FinishLoginUseCase.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.daniebeler.pfpixelix.domain.usecase
-
-import com.daniebeler.pfpixelix.di.HostSelectionInterceptorInterface
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
-import me.tatarka.inject.annotations.Inject
-
-@Inject
-class FinishLoginUseCase constructor(
- private val authRepository: AuthRepository,
- private val hostSelectionInterceptorInterface: HostSelectionInterceptorInterface
-) {
- suspend operator fun invoke(loginData: LoginData, currentlyLoggedIn: String) {
- if (loginData.baseUrl.isNotBlank()) {
- hostSelectionInterceptorInterface.setHost(loginData.baseUrl.replace("https://", "")) }
- if (loginData.accessToken.isNotBlank()) {
- hostSelectionInterceptorInterface.setToken(loginData.accessToken)
- }
- authRepository.finishLogin(loginData, currentlyLoggedIn)
- }
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetAuthDataUseCase.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetAuthDataUseCase.kt
deleted file mode 100644
index 2fe42888..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetAuthDataUseCase.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.daniebeler.pfpixelix.domain.usecase
-
-import com.daniebeler.pfpixelix.domain.model.AuthData
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
-import me.tatarka.inject.annotations.Inject
-
-@Inject
-class GetAuthDataUseCase(private val repository: AuthRepository) {
- suspend operator fun invoke(): AuthData {
- return repository.getAuthData()
- }
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetCurrentLoginDataUseCase.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetCurrentLoginDataUseCase.kt
index edc90b5a..a2c93ee6 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetCurrentLoginDataUseCase.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetCurrentLoginDataUseCase.kt
@@ -1,12 +1,12 @@
package com.daniebeler.pfpixelix.domain.usecase
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
+import com.daniebeler.pfpixelix.domain.service.session.AuthService
+import com.daniebeler.pfpixelix.domain.service.session.Credentials
import me.tatarka.inject.annotations.Inject
@Inject
-class GetCurrentLoginDataUseCase constructor(private val repository: AuthRepository) {
- suspend operator fun invoke(): LoginData? {
- return repository.getCurrentLoginData()
+class GetCurrentLoginDataUseCase constructor(private val authService: AuthService) {
+ suspend operator fun invoke(): Credentials? {
+ return authService.getCurrentSession()
}
}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetOwnInstanceDomainUseCase.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetOwnInstanceDomainUseCase.kt
index 1d08e0a3..1a48fdf6 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetOwnInstanceDomainUseCase.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/GetOwnInstanceDomainUseCase.kt
@@ -1,17 +1,14 @@
package com.daniebeler.pfpixelix.domain.usecase
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
+import com.daniebeler.pfpixelix.domain.service.session.AuthService
import me.tatarka.inject.annotations.Inject
@Inject
class GetOwnInstanceDomainUseCase(
- private val repository: AuthRepository
+ private val authService: AuthService
) {
suspend operator fun invoke(): String {
- val currentLoginData = repository.getCurrentLoginData()
- currentLoginData?.let {
- return repository.getCurrentLoginData()!!.baseUrl.substringAfter("https://")
- }
- return ("https://err.or")
+ val currentLoginData = authService.getCurrentSession()!!
+ return currentLoginData.serverUrl
}
}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/LogoutUseCase.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/LogoutUseCase.kt
index ec3ae252..5cbc5828 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/LogoutUseCase.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/LogoutUseCase.kt
@@ -1,16 +1,16 @@
package com.daniebeler.pfpixelix.domain.usecase
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
import com.daniebeler.pfpixelix.domain.repository.SavedSearchesRepository
+import com.daniebeler.pfpixelix.domain.service.session.AuthService
import me.tatarka.inject.annotations.Inject
@Inject
class LogoutUseCase(
- private val authRepository: AuthRepository,
+ private val authService: AuthService,
private val savedSearchesRepository: SavedSearchesRepository
) {
suspend operator fun invoke() {
- authRepository.logout()
+ authService.deleteSession()
savedSearchesRepository.clearSavedSearches()
}
}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateCurrentUserUseCase.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateCurrentUserUseCase.kt
deleted file mode 100644
index 11eb15c8..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateCurrentUserUseCase.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.daniebeler.pfpixelix.domain.usecase
-
-import com.daniebeler.pfpixelix.di.HostSelectionInterceptorInterface
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
-import me.tatarka.inject.annotations.Inject
-
-@Inject
-class UpdateCurrentUserUseCase(private val authRepository: AuthRepository, private val hostSelectionInterceptorInterface: HostSelectionInterceptorInterface) {
- suspend operator fun invoke(newLoginData: LoginData) {
- hostSelectionInterceptorInterface.setHost(newLoginData.baseUrl)
- hostSelectionInterceptorInterface.setToken(newLoginData.accessToken)
- authRepository.updateCurrentUser(newLoginData.accountId)
- }
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateLoginDataUseCase.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateLoginDataUseCase.kt
deleted file mode 100644
index a8e86a44..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/domain/usecase/UpdateLoginDataUseCase.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.daniebeler.pfpixelix.domain.usecase
-
-import com.daniebeler.pfpixelix.di.HostSelectionInterceptorInterface
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.repository.AuthRepository
-import me.tatarka.inject.annotations.Inject
-
-@Inject
-class UpdateLoginDataUseCase constructor(
- private val authRepository: AuthRepository,
- private val hostSelectionInterceptorInterface: HostSelectionInterceptorInterface
-) {
- suspend operator fun invoke(loginData: LoginData) {
- if (loginData.baseUrl.isNotBlank()) {
- hostSelectionInterceptorInterface.setHost(loginData.baseUrl.replace("https://", "")) }
- if (loginData.accessToken.isNotBlank()) {
- hostSelectionInterceptorInterface.setToken(loginData.accessToken)
- }
- authRepository.updateOngoingLoginData(loginData)
- }
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/LoginComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/LoginComposable.kt
deleted file mode 100644
index f1c22ba0..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/LoginComposable.kt
+++ /dev/null
@@ -1,253 +0,0 @@
-package com.daniebeler.pfpixelix.ui.composables
-
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-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.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.foundation.text.KeyboardOptions
-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.CircularProgressIndicator
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TextField
-import androidx.compose.material3.TextFieldDefaults
-import androidx.compose.runtime.Composable
-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.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextDecoration
-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.Navigate
-import com.daniebeler.pfpixelix.utils.imeAwareInsets
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import org.jetbrains.compose.resources.painterResource
-import org.jetbrains.compose.resources.stringResource
-import pixelix.app.generated.resources.Res
-import pixelix.app.generated.resources.i_don_t_have_an_account
-import pixelix.app.generated.resources.login_wave_dark
-import pixelix.app.generated.resources.login_wave_light
-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
-
-@Composable
-fun LoginComposable(
- isLoading: Boolean,
- error: String,
- viewModel: LoginViewModel = injectViewModel(key = "login-viewmodel-key") { loginViewModel }
-) {
- val context = LocalKmpContext.current
- val keyboardController = LocalSoftwareKeyboardController.current
- val focusManager = LocalFocusManager.current
-
- Scaffold { paddingValues ->
- Column(
- modifier = Modifier
- .padding(paddingValues)
- .fillMaxWidth()
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .background(
- if (isSystemInDarkTheme()) {
- Color.White
- } else {
- Color.Black
- }
- ), horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Spacer(modifier = Modifier.height(50.dp))
- Image(
- painterResource(
- if (isSystemInDarkTheme()) {
- Res.drawable.pixelix_logo_black_xxl
- } else {
- Res.drawable.pixelix_logo_white_xxl
- }
- ), contentDescription = null,
- Modifier
- .size(150.dp)
- .clip(CircleShape)
- )
-
- Spacer(modifier = Modifier.height(12.dp))
-
- Text(
- text = "PIXELIX",
- fontSize = 38.sp,
- fontWeight = FontWeight.Black,
- color = if (isSystemInDarkTheme()) {
- Color.Black
- } else {
- Color.White
- }
- )
-
- Spacer(modifier = Modifier.height(24.dp))
- }
-
- Image(
- painterResource(
- if (isSystemInDarkTheme()) {
- Res.drawable.login_wave_light
- } else {
- Res.drawable.login_wave_dark
- }
- ),
- contentDescription = null,
- contentScale = ContentScale.FillWidth,
- modifier = Modifier.fillMaxWidth()
- )
-
- if (isLoading) {
- Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
- CircularProgressIndicator()
- }
- } else if (error.isNotBlank()) {
- Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
- Text(text = error)
- }
- } else {
- Spacer(modifier = Modifier.weight(1f))
- Column(Modifier.padding(12.dp)) {
-
- Row {
- Spacer(Modifier.width(6.dp))
- Text(
- text = stringResource(Res.string.server_url), fontWeight = FontWeight.Bold
- )
- }
-
- Spacer(Modifier.height(6.dp))
-
- Row(verticalAlignment = Alignment.Bottom) {
-
-
- TextField(
- value = viewModel.customUrl,
- prefix = { Text("https://") },
- singleLine = true,
- onValueChange = {
- viewModel.customUrl = it
- viewModel.domainChanged()
- },
- modifier = Modifier.weight(1f),
- shape = RoundedCornerShape(16.dp),
- colors = TextFieldDefaults.colors(
- unfocusedIndicatorColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer,
- unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer
- ),
- keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
- keyboardActions = KeyboardActions(onDone = {
- keyboardController?.hide()
- focusManager.clearFocus()
- CoroutineScope(Dispatchers.Default).launch {
- viewModel.login(viewModel.customUrl, context)
- }
- })
- )
-
- Spacer(Modifier.width(12.dp))
- if (viewModel.loading) {
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier
- .height(56.dp)
- .width(56.dp)
- .padding(0.dp, 0.dp)
- .clip(RoundedCornerShape(16.dp))
- .background(MaterialTheme.colorScheme.primary)
-
- ) {
- CircularProgressIndicator(
- modifier = Modifier.size(24.dp),
- color = MaterialTheme.colorScheme.onPrimary
- )
- }
- } else {
- Button(
- onClick = {
- CoroutineScope(Dispatchers.Default).launch {
- viewModel.login(viewModel.customUrl, context)
- }
- },
- Modifier
- .height(56.dp)
- .width(56.dp)
- .padding(0.dp, 0.dp),
- shape = RoundedCornerShape(16.dp),
- contentPadding = PaddingValues(12.dp),
- enabled = viewModel.isValidUrl,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.primary,
- contentColor = MaterialTheme.colorScheme.onPrimary,
- disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer
- )
- ) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos,
- contentDescription = "submit",
- Modifier
- .fillMaxSize()
- .fillMaxWidth()
- )
- }
- }
- }
- Spacer(modifier = Modifier.height(24.dp))
-
- TextButton(onClick = {
- val url = "https://pixelfed.org/servers"
- Navigate.openUrlInApp(context, url)
- }) {
- Text(
- stringResource(Res.string.i_don_t_have_an_account),
- textDecoration = TextDecoration.Underline,
- textAlign = TextAlign.Center,
- modifier = Modifier.fillMaxWidth()
- )
- }
-
- }
-
- Spacer(modifier = Modifier.height(200.dp))
- Spacer(modifier = Modifier.imeAwareInsets(200.dp))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/LoginViewModel.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/LoginViewModel.kt
deleted file mode 100644
index f84d46d3..00000000
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/LoginViewModel.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-package com.daniebeler.pfpixelix.ui.composables
-
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.lifecycle.ViewModel
-import com.daniebeler.pfpixelix.domain.model.Application
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.repository.CountryRepository
-import com.daniebeler.pfpixelix.domain.usecase.AddNewLoginUseCase
-import com.daniebeler.pfpixelix.domain.usecase.UpdateLoginDataUseCase
-import com.daniebeler.pfpixelix.utils.KmpContext
-import com.daniebeler.pfpixelix.utils.Navigate
-import me.tatarka.inject.annotations.Inject
-
-class LoginViewModel @Inject constructor(
- private val repository: CountryRepository,
- private val newLoginDataUseCase: AddNewLoginUseCase,
- private val updateLoginDataUseCase: UpdateLoginDataUseCase
-) : ViewModel() {
-
- private val domainRegex: Regex =
- "^((\\*)|((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|((\\*\\.)?([a-zA-Z0-9-]+\\.){0,5}[a-zA-Z0-9-][a-zA-Z0-9-]+\\.[a-zA-Z]{2,63}?))\$".toRegex()
-
- private var _authApplication: Application? = null
- var customUrl: String by mutableStateOf("")
- var isValidUrl: Boolean by mutableStateOf(false)
-
- var loading: Boolean by mutableStateOf(false)
-
-
- private suspend fun registerApplication(): String {
- _authApplication = repository.createApplication()
- if (_authApplication != null) {
- return _authApplication!!.clientId
- }
- return ""
- }
-
- private suspend fun setBaseUrl(_baseUrl: String): String {
- var baseUrl = _baseUrl
- if (!baseUrl.startsWith("https://")) {
- baseUrl = "https://$baseUrl"
- }
- newLoginDataUseCase(LoginData(baseUrl = baseUrl, loginOngoing = true))
- return baseUrl
- }
-
- fun domainChanged() {
- isValidUrl = domainRegex.matches(customUrl)
- }
-
- suspend fun login(baseUrl: String, context: KmpContext) {
- loading = true
- if (domainRegex.matches(baseUrl)) {
- val newBaseUrl = setBaseUrl(baseUrl)
- val authApplication = repository.createApplication()
- if (authApplication != null) {
- updateLoginDataUseCase(
- LoginData(
- baseUrl = baseUrl,
- clientId = authApplication.clientId,
- clientSecret = authApplication.clientSecret,
- loginOngoing = true
- )
- )
- openUrl(context, authApplication.clientId, newBaseUrl)
- }
- }
- loading = false
- }
-
- private fun openUrl(context: KmpContext, clientId: String, baseUrl: String) {
- val url =
- "${baseUrl}/oauth/authorize?response_type=code&redirect_uri=pixelix-android-auth://callback&client_id=" + clientId
- Navigate.openUrlInApp(context, url)
- }
-}
\ No newline at end of file
diff --git a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.kt b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.kt
index de1530c2..dce75961 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/newpost/NewPostComposable.kt
@@ -1,6 +1,5 @@
package com.daniebeler.pfpixelix.ui.composables.newpost
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -65,12 +64,10 @@ import com.daniebeler.pfpixelix.ui.composables.textfield_mentions.TextFieldMenti
import com.daniebeler.pfpixelix.utils.KmpUri
import com.daniebeler.pfpixelix.utils.LocalKmpContext
import com.daniebeler.pfpixelix.utils.MimeType
-import com.daniebeler.pfpixelix.utils.Navigate
import com.daniebeler.pfpixelix.utils.imeAwareInsets
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
import pixelix.app.generated.resources.Res
-import pixelix.app.generated.resources.add_outline
import pixelix.app.generated.resources.alt_text
import pixelix.app.generated.resources.audience
import pixelix.app.generated.resources.audience_public
@@ -199,7 +196,6 @@ fun NewPostComposable(
}
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
SinglePhotoPickerButton { result ->
- Navigate.navigate("new_post_screen", navController)
result.forEach {
viewModel.addImage(it, context)
}
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 db3fc456..7a88fdaf 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
@@ -28,11 +28,11 @@ import androidx.compose.ui.draw.clip
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.common.Destinations
import com.daniebeler.pfpixelix.di.injectViewModel
-import com.daniebeler.pfpixelix.domain.model.loginDataToAccount
+import com.daniebeler.pfpixelix.domain.model.credentialsToAccount
import com.daniebeler.pfpixelix.ui.composables.custom_account.CustomAccount
-import com.daniebeler.pfpixelix.utils.LocalKmpContext
-import com.daniebeler.pfpixelix.utils.openLoginScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -48,31 +48,38 @@ import pixelix.app.generated.resources.remove_account
@Composable
fun AccountSwitchBottomSheet(
+ navController: NavController,
closeBottomSheet: () -> Unit,
ownProfileViewModel: OwnProfileViewModel?,
viewModel: AccountSwitchViewModel = injectViewModel(key = "account_switcher_viewmodel") { accountSwitchViewModel }
) {
- val context = LocalKmpContext.current
val showRemoveLoginDataAlert = remember { mutableStateOf("") }
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- if (viewModel.currentlyLoggedIn.currentAccount != null) {
+ val sessionStorage = viewModel.sessionStorage
+ val currentlyLoggedIn = remember(sessionStorage) {
+ viewModel.sessionStorage?.getActiveSession()
+ }
+ if (currentlyLoggedIn != null) {
Text(
text = stringResource(Res.string.current_account),
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
modifier = Modifier.padding(start = 12.dp)
)
- CustomAccount(account = loginDataToAccount(viewModel.currentlyLoggedIn.currentAccount!!))
+ CustomAccount(account = credentialsToAccount(currentlyLoggedIn))
+ }
+ val otherAccounts = remember(sessionStorage) {
+ viewModel.sessionStorage?.sessions?.filter { it != currentlyLoggedIn }.orEmpty()
}
- if (viewModel.otherAccounts.otherAccounts.isNotEmpty()) {
+ if (otherAccounts.isNotEmpty()) {
Text(
text = stringResource(Res.string.other_accounts),
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
modifier = Modifier.padding(start = 12.dp)
)
- viewModel.otherAccounts.otherAccounts.map { otherAccount ->
+ otherAccounts.map { otherAccount ->
Box(Modifier.clickable {
viewModel.switchAccount(otherAccount) {
ownProfileViewModel?.let {
@@ -82,9 +89,9 @@ fun AccountSwitchBottomSheet(
}
}) {
CustomAccount(
- account = loginDataToAccount(otherAccount),
+ account = credentialsToAccount(otherAccount),
logoutButton = true,
- logout = {showRemoveLoginDataAlert.value = otherAccount.accountId})
+ logout = { showRemoveLoginDataAlert.value = otherAccount.accountId })
}
}
}
@@ -92,7 +99,10 @@ fun AccountSwitchBottomSheet(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 8.dp)
.fillMaxWidth()
- .clickable { context.openLoginScreen(true) },
+ .clickable {
+ navController.navigate(Destinations.NewLogin.route)
+ closeBottomSheet()
+ },
verticalAlignment = Alignment.CenterVertically
) {
Box(
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 41099008..ceab7783 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
@@ -5,21 +5,17 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.daniebeler.pfpixelix.domain.model.LoginData
-import com.daniebeler.pfpixelix.domain.usecase.GetAuthDataUseCase
-import com.daniebeler.pfpixelix.domain.usecase.RemoveLoginDataUseCase
-import com.daniebeler.pfpixelix.domain.usecase.UpdateCurrentUserUseCase
+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.launch
import me.tatarka.inject.annotations.Inject
class AccountSwitchViewModel @Inject constructor(
- private val getAuthDataUseCase: GetAuthDataUseCase,
- private val updateCurrentUserUseCase: UpdateCurrentUserUseCase,
- private val removeLoginDataUseCase: RemoveLoginDataUseCase
+ private val authService: AuthService
) : ViewModel() {
- var currentlyLoggedIn: CurrentAccountState by mutableStateOf(CurrentAccountState())
- var otherAccounts: OtherAccountsState by mutableStateOf(OtherAccountsState())
+ var sessionStorage by mutableStateOf(null)
init {
loadAccounts()
@@ -27,18 +23,13 @@ class AccountSwitchViewModel @Inject constructor(
private fun loadAccounts() {
viewModelScope.launch {
- val authData = getAuthDataUseCase()
- currentlyLoggedIn =
- CurrentAccountState(currentAccount = authData.loginDataList.find { it.accountId == authData.currentlyLoggedIn }
- ?: LoginData())
- otherAccounts =
- OtherAccountsState(otherAccounts = authData.loginDataList.filter { it.accountId != authData.currentlyLoggedIn && !it.loginOngoing })
+ sessionStorage = authService.getAvailableSessions()
}
}
- fun switchAccount(newAccount: LoginData, changedAccount: () -> Unit) {
+ fun switchAccount(newAccount: Credentials, changedAccount: () -> Unit) {
val coroutine = viewModelScope.launch {
- updateCurrentUserUseCase(newAccount)
+ authService.openSessionIfExist(userId = newAccount.accountId)
}
coroutine.invokeOnCompletion {
@@ -50,7 +41,7 @@ class AccountSwitchViewModel @Inject constructor(
fun removeAccount(accountId: String) {
val coroutine = viewModelScope.launch {
- removeLoginDataUseCase(accountId)
+ authService.deleteSession(accountId)
}
coroutine.invokeOnCompletion {
loadAccounts()
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 e2f1b500..1c0e7d0a 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
@@ -232,9 +232,11 @@ fun OwnProfileComposable(
showBottomSheet = 0
}, openPreferencesDrawer)
} else if (showBottomSheet == 2) {
- AccountSwitchBottomSheet(closeBottomSheet = {
- showBottomSheet = 0
- }, viewModel)
+ AccountSwitchBottomSheet(
+ navController = navController,
+ closeBottomSheet = { showBottomSheet = 0 },
+ viewModel
+ )
}
}
}
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
new file mode 100644
index 00000000..82955c29
--- /dev/null
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginComposable.kt
@@ -0,0 +1,243 @@
+package com.daniebeler.pfpixelix.ui.composables.session
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+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.width
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+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.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+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.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.navigation.NavController
+import com.daniebeler.pfpixelix.common.Destinations
+import com.daniebeler.pfpixelix.di.injectViewModel
+import com.daniebeler.pfpixelix.utils.LocalKmpContext
+import com.daniebeler.pfpixelix.utils.Navigate
+import com.daniebeler.pfpixelix.utils.imeAwareInsets
+import kotlinx.coroutines.launch
+import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.resources.stringResource
+import pixelix.app.generated.resources.Res
+import pixelix.app.generated.resources.i_don_t_have_an_account
+import pixelix.app.generated.resources.login_wave_dark
+import pixelix.app.generated.resources.login_wave_light
+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
+
+@Composable
+fun LoginComposable(
+ viewModel: LoginViewModel = injectViewModel("LoginViewModel") { loginViewModel }
+) {
+ Scaffold(Modifier.fillMaxSize()) { paddingValues ->
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ if (isSystemInDarkTheme()) Color.White else Color.Black
+ )
+ .windowInsetsPadding(
+ WindowInsets.systemBars.only(
+ WindowInsetsSides.Top + WindowInsetsSides.Horizontal
+ )
+ ),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(50.dp))
+ Image(
+ modifier = Modifier
+ .size(150.dp)
+ .clip(CircleShape),
+ painter = painterResource(
+ if (isSystemInDarkTheme()) {
+ Res.drawable.pixelix_logo_black_xxl
+ } else {
+ Res.drawable.pixelix_logo_white_xxl
+ }
+ ),
+ contentDescription = null
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Text(
+ text = "PIXELIX",
+ fontSize = 38.sp,
+ fontWeight = FontWeight.Black,
+ color = if (isSystemInDarkTheme()) Color.Black else Color.White
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+ }
+
+ Image(
+ painterResource(
+ if (isSystemInDarkTheme()) {
+ Res.drawable.login_wave_light
+ } else {
+ Res.drawable.login_wave_dark
+ }
+ ),
+ contentDescription = null,
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ viewModel.error?.let { err ->
+ if (err.isNotBlank()) {
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
+ Text(text = err)
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.weight(1f))
+ Column(Modifier.padding(12.dp)) {
+
+ Row {
+ Spacer(Modifier.width(6.dp))
+ Text(
+ text = stringResource(Res.string.server_url),
+ fontWeight = FontWeight.Bold
+ )
+ }
+
+ Spacer(Modifier.height(6.dp))
+
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusManager = LocalFocusManager.current
+ fun login() {
+ keyboardController?.hide()
+ focusManager.clearFocus()
+ viewModel.auth()
+ }
+
+ Row(verticalAlignment = Alignment.Bottom) {
+ TextField(
+ value = viewModel.serverHost,
+ onValueChange = { viewModel.updateServerHost(it) },
+ prefix = { Text("https://") },
+ singleLine = true,
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(16.dp),
+ colors = TextFieldDefaults.colors(
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer,
+ unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer
+ ),
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ keyboardActions = KeyboardActions(onDone = { login() })
+ )
+
+ Spacer(Modifier.width(12.dp))
+ if (viewModel.isLoading) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .height(56.dp)
+ .width(56.dp)
+ .padding(0.dp, 0.dp)
+ .clip(RoundedCornerShape(16.dp))
+ .background(MaterialTheme.colorScheme.primary)
+
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(24.dp),
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+ } else {
+ Button(
+ onClick = { login() },
+ Modifier
+ .height(56.dp)
+ .width(56.dp)
+ .padding(0.dp, 0.dp),
+ shape = RoundedCornerShape(16.dp),
+ contentPadding = PaddingValues(12.dp),
+ enabled = viewModel.isValidHost,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = MaterialTheme.colorScheme.onPrimary,
+ disabledContainerColor = MaterialTheme.colorScheme.surfaceContainer
+ )
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowForwardIos,
+ contentDescription = "submit",
+ Modifier
+ .fillMaxSize()
+ .fillMaxWidth()
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(24.dp))
+
+ val context = LocalKmpContext.current
+ TextButton(onClick = {
+ val url = "https://pixelfed.org/servers"
+ Navigate.openUrlInApp(context, url)
+ }) {
+ Text(
+ stringResource(Res.string.i_don_t_have_an_account),
+ textDecoration = TextDecoration.Underline,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(200.dp))
+ Spacer(modifier = Modifier.imeAwareInsets(200.dp))
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..bf08f489
--- /dev/null
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/ui/composables/session/LoginViewModel.kt
@@ -0,0 +1,47 @@
+package com.daniebeler.pfpixelix.ui.composables.session
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.daniebeler.pfpixelix.domain.service.session.AuthService
+import kotlinx.coroutines.launch
+import me.tatarka.inject.annotations.Inject
+
+@Inject
+class LoginViewModel(
+ private val authService: AuthService
+) : ViewModel() {
+
+ var serverHost by mutableStateOf("")
+ private set
+
+ var isLoading by mutableStateOf(false)
+ private set
+
+ var isValidHost by mutableStateOf(false)
+ private set
+
+ var error by mutableStateOf(null)
+ private set
+
+ fun updateServerHost(host: String) {
+ serverHost = host
+ isValidHost = authService.isValidHost(serverHost)
+ }
+
+ fun auth() {
+ viewModelScope.launch {
+ try {
+ isLoading = true
+ error = null
+ authService.auth(serverHost)
+ } catch (e: Exception) {
+ error = e.message
+ } finally {
+ isLoading = false
+ }
+ }
+ }
+}
\ 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 46b30531..c76e5218 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
@@ -10,7 +10,6 @@ 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 com.daniebeler.pfpixelix.utils.openLoginScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -37,11 +36,6 @@ fun LogoutPref(logout: () -> Unit) {
)
}
-@Composable
-fun LogoutPrefPreview() {
- LogoutPref { }
-}
-
@Composable
fun LogoutAlert(show: MutableState, logout: () -> Unit) {
val context = LocalKmpContext.current
@@ -56,7 +50,6 @@ fun LogoutAlert(show: MutableState, logout: () -> Unit) {
TextButton(onClick = {
CoroutineScope(Dispatchers.Default).launch {
logout()
- context.openLoginScreen()
}
}) {
Text(stringResource(Res.string.logout))
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 dd6bc8ee..3d8c2b8b 100644
--- a/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt
+++ b/app/src/commonMain/kotlin/com/daniebeler/pfpixelix/utils/KmpPlatform.kt
@@ -24,7 +24,6 @@ expect val KmpContext.appVersionName: String
expect fun KmpContext.openUrlInApp(url: String)
expect fun KmpContext.openUrlInBrowser(url: String)
expect fun KmpContext.setDefaultNightMode(mode: Int)
-expect fun KmpContext.openLoginScreen(isAbleToGotBack: Boolean = false)
expect fun KmpContext.getCacheSizeInBytes(): Long
expect fun KmpContext.cleanCache()
expect fun KmpContext.getAppIcons(): List
diff --git a/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/App.ios.kt b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/App.ios.kt
new file mode 100644
index 00000000..ac07aab5
--- /dev/null
+++ b/app/src/iosMain/kotlin/com/daniebeler/pfpixelix/App.ios.kt
@@ -0,0 +1,7 @@
+package com.daniebeler.pfpixelix
+
+import androidx.compose.runtime.Composable
+
+@Composable
+actual fun SetUpEdgeToEdgeDialog() {
+}
\ 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 b1d626e0..78e8a27b 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
@@ -32,9 +32,6 @@ actual val KmpContext.appVersionName: String
actual fun KmpContext.setDefaultNightMode(mode: Int) {
}
-actual fun KmpContext.openLoginScreen(isAbleToGotBack: Boolean) {
-}
-
actual fun KmpContext.getCacheSizeInBytes(): Long {
TODO("Not yet implemented")
}