Basic profile editing

This commit is contained in:
Matthieu 2022-10-30 20:51:09 +01:00
parent e35cb17879
commit 2497504530
10 changed files with 476 additions and 44 deletions

View File

@ -9,6 +9,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
// Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155
jacoco.toolVersion = "0.8.7"

View File

@ -1,21 +1,26 @@
package org.pixeldroid.app.profile
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityEditProfileBinding
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.openUrl
class EditProfileActivity : BaseActivity() {
@ -35,18 +40,89 @@ class EditProfileActivity : BaseActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
binding.savingProgressBar.visibility = if(uiState.loadingProfile) View.VISIBLE else View.INVISIBLE
if(uiState.profileLoaded){
binding.bioEditText.setText(uiState.bio)
binding.nameEditText.setText(uiState.name)
model.changesApplied()
}
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
if(uiState.loadingProfile) binding.progressText.setText(R.string.fetching_profile)
else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile)
binding.privateSwitch.isChecked = uiState.privateAccount == true
Glide.with(binding.profilePic).load(uiState.profilePictureUri)
.apply(RequestOptions.circleCropTransform())
.into(binding.profilePic)
binding.savingProgressBar.visibility = if(uiState.error || uiState.profileSent) View.GONE
else View.VISIBLE
if(uiState.profileSent){
binding.progressText.setText(R.string.profile_saved)
binding.done.visibility = View.VISIBLE
} else {
binding.done.visibility = View.GONE
}
if(uiState.error){
Snackbar.make(binding.root, "Something went wrong",
binding.progressText.setText(R.string.error_profile)
binding.error.visibility = View.VISIBLE
} else binding.error.visibility = View.GONE
}
}
}
binding.bioEditText.doAfterTextChanged {
model.updateBio(binding.bioEditText.text)
}
binding.nameEditText.doAfterTextChanged {
model.updateName(binding.nameEditText.text)
}
binding.privateSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updatePrivate(isChecked)
}
binding.progressCard.setOnClickListener {
model.clickedCard()
}
binding.editButton.setOnClickListener {
val domain = db.userDao().getActiveUser()!!.instance_uri
val url = "$domain/settings/home"
if(!openUrl(url)) {
Snackbar.make(binding.root, getString(R.string.edit_link_failed),
Snackbar.LENGTH_LONG).show()
model.errorShown()
}
}
// binding.changeImageButton.setOnClickListener {
// Intent(Intent.ACTION_GET_CONTENT).apply {
// type = "*/*"
// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
// action = Intent.ACTION_GET_CONTENT
// addCategory(Intent.CATEGORY_OPENABLE)
// putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
// uploadImageResultContract.launch(
// Intent.createChooser(this, null)
// )
// }
// }
}
private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val data: Intent? = result.data
if (result.resultCode == Activity.RESULT_OK && data != null) {
val images: ArrayList<String> = ArrayList()
val clipData = data.clipData
if (clipData != null) {
val count = clipData.itemCount
for (i in 0 until count) {
val imageUri: String = clipData.getItemAt(i).uri.toString()
images.add(imageUri)
}
model.uploadImage(images.first())
} else if (data.data != null) {
images.add(data.data!!.toString())
model.uploadImage(images.first())
}
}
}
@ -56,13 +132,22 @@ class EditProfileActivity : BaseActivity() {
return true
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
if(model.madeChanges()){
AlertDialog.Builder(binding.root.context).apply {
setMessage(getString(R.string.profile_save_changes))
setNegativeButton(android.R.string.cancel) { _, _ -> }
setPositiveButton(android.R.string.ok) { _, _ -> super.onBackPressed()}
}.show()
}
else super.onBackPressed()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){
R.id.action_apply -> {
model.apply(
binding.nameEditText.text.toString(),
binding.bioEditText.text.toString(),
)
model.sendProfile()
return true
}
}

View File

@ -2,24 +2,38 @@ package org.pixeldroid.app.profile
import android.app.Application
import android.net.Uri
import android.provider.OpenableColumns
import android.text.Editable
import android.util.Log
import android.widget.Toast
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.RequestBody
import org.pixeldroid.app.R
import org.pixeldroid.app.postCreation.ProgressRequestBody
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import retrofit2.HttpException
import java.io.File
import java.io.IOException
import java.lang.Exception
import java.net.URI
import javax.inject.Inject
class EditProfileViewModel(application: Application) : AndroidViewModel(application) {
@ -41,7 +55,8 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
viewModelScope.launch {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
try {
oldProfile = api.verifyCredentials()
val profile = api.verifyCredentials()
if (oldProfile == null) oldProfile = profile
_uiState.update { currentUiState ->
currentUiState.copy(
name = oldProfile?.display_name,
@ -49,53 +64,94 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
profilePictureUri = oldProfile?.anyAvatar()?.toUri(),
privateAccount = oldProfile?.locked,
loadingProfile = false,
sendingProfile = false
sendingProfile = false,
profileLoaded = true,
error = false
)
}
} catch (exception: IOException) {
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = false,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true
)
}
} catch (exception: HttpException) {
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = false,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true
)
}
}
}
}
fun apply(name: String, bio: String) {
//TODO check if name and bio have changed, else send null to updatecredentials or don't update at all
_uiState.update { currentUiState ->
if(oldProfile != null) currentUiState.copy(name = name, bio = bio, sendingProfile = true, loadingProfile = false)
else currentUiState.copy(name = name, bio = bio, sendingProfile = false)
}
if(oldProfile == null) return
fun sendProfile() {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val requestBody = null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody)
val requestBody =
null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody)
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = true,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = false
)
}
viewModelScope.launch {
with(uiState.value) {
try {
api.updateCredentials(
val account = api.updateCredentials(
displayName = name,
note = bio,
locked = privateAccount,
// avatar = requestBody
)
oldProfile = account
_uiState.update { currentUiState ->
currentUiState.copy(
bio = account.note,
name = account.display_name,
profilePictureUri = account.anyAvatar()?.toUri(),
privateAccount = account.locked,
sendingProfile = false,
profileSent = true,
loadingProfile = false,
profileLoaded = true,
error = false
)
}
} catch (exception: IOException) {
Log.e("TAG", exception.toString())
_uiState.update { currentUiState ->
currentUiState.copy(error = true)
currentUiState.copy(
sendingProfile = false,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true
)
}
} catch (exception: HttpException) {
Log.e("TAG", exception.toString())
_uiState.update { currentUiState ->
currentUiState.copy(error = true)
currentUiState.copy(
sendingProfile = false,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true
)
}
} catch (exception: Exception) {
Log.e("TAG", exception.toString())
@ -110,7 +166,137 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
currentUiState.copy(error = false)
}
}
fun updateBio(bio: Editable?) {
_uiState.update { currentUiState ->
currentUiState.copy(bio = bio.toString())
}
}
fun updateName(name: Editable?) {
_uiState.update { currentUiState ->
currentUiState.copy(name = name.toString())
}
}
fun updatePrivate(isChecked: Boolean) {
_uiState.update { currentUiState ->
currentUiState.copy(privateAccount = isChecked)
}
}
fun changesApplied() {
_uiState.update { currentUiState ->
currentUiState.copy(profileLoaded = false)
}
}
fun madeChanges(): Boolean =
with(uiState.value) {
oldProfile?.locked != privateAccount
|| oldProfile?.display_name != name || oldProfile?.note != bio
}
fun clickedCard() {
if (uiState.value.error) {
if (!uiState.value.profileLoaded) {
// Load failed
loadProfile()
} else if (uiState.value.profileLoaded) {
// Send failed
sendProfile()
}
} else {
// Dismiss success card
_uiState.update { currentUiState ->
currentUiState.copy(profileSent = false)
}
}
}
fun uploadImage(image: String) {
//TODO fix
val inputStream =
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image.toUri())
?: return
val size: Long =
if (image.toUri().scheme == "content") {
getApplication<PixelDroidApplication>().contentResolver.query(
image.toUri(),
null,
null,
null,
null
)
?.use { cursor ->
/* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
cursor.getLong(sizeIndex)
} ?: 0
} else {
image.toUri().toFile().length()
}
val imagePart = ProgressRequestBody(inputStream, size, "image/*")
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("avatar", System.currentTimeMillis().toString(), imagePart)
.build()
val sub = imagePart.progressSubject
.subscribeOn(Schedulers.io())
.subscribe { percentage ->
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = percentage.toInt()
)
}
}
var postSub: Disposable? = null
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val inter = api.updateProfilePicture(requestBody.parts[0])
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ it: Account ->
Log.e("qsdfqsdfs", it.toString())
},
{ e: Throwable ->
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = 0,
uploadingPicture = true,
error = true
)
}
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
{
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = 100,
uploadingPicture = false
)
}
postSub?.dispose()
sub.dispose()
}
)
}
}
data class EditProfileActivityUiState(
val name: String? = null,
@ -118,8 +304,12 @@ data class EditProfileActivityUiState(
val profilePictureUri: Uri?= null,
val privateAccount: Boolean? = null,
val loadingProfile: Boolean = true,
val profileLoaded: Boolean = false,
val sendingProfile: Boolean = false,
val error: Boolean = false
val profileSent: Boolean = false,
val error: Boolean = false,
val uploadingPicture: Boolean = false,
val uploadProgress: Int = 0,
)
class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory {

View File

@ -295,15 +295,21 @@ interface PixelfedAPI {
@Header("Authorization") authorization: String? = null
): Account
@Multipart
//@Multipart
@PATCH("/api/v1/accounts/update_credentials")
suspend fun updateCredentials(
@Part(value = "display_name") displayName: String?,
@Part(value = "note") note: String?,
@Part(value = "locked") locked: Boolean?,
@Query(value = "display_name") displayName: String?,
@Query(value = "note") note: String?,
@Query(value = "locked") locked: Boolean?,
// @Part avatar: MultipartBody.Part?,
): Account
@Multipart
@PATCH("/api/v1/accounts/update_credentials")
fun updateProfilePicture(
@Part avatar: MultipartBody.Part?
): Observable<Account>
@GET("/api/v1/accounts/{id}/statuses")
suspend fun accountPosts(
@Path("id") account_id: String,

View File

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
<path android:fillColor="?attr/colorOnPrimaryContainer" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

View File

@ -14,7 +14,9 @@
app:layout_constraintBottom_toTopOf="@+id/textInputLayoutName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
tools:srcCompat="@tools:sample/avatars"
android:contentDescription="@string/profile_picture" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayoutName"
@ -32,7 +34,7 @@
android:id="@+id/nameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Your Name"
android:hint="@string/your_name"
android:ems="10"
android:imeOptions="actionDone" />
</com.google.android.material.textfield.TextInputLayout>
@ -53,17 +55,130 @@
android:id="@+id/bioEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Add a bio here" />
android:hint="@string/your_bio" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/privateSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/privateText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutBio" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/privateText"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/privateSwitch"
app:layout_constraintTop_toTopOf="@+id/privateSwitch"
app:layout_constraintBottom_toBottomOf="@+id/privateSwitch">
<TextView
android:id="@+id/privateTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/private_account"
android:textStyle="bold"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/private_account_explanation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/privateTitle"
app:layout_constraintTop_toBottomOf="@+id/privateTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Button
android:id="@+id/editButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/more_profile_settings"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:icon="@drawable/ic_baseline_open_in_browser_24"
app:layout_constraintTop_toBottomOf="@+id/privateText" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/progressCard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_margin="8dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/progressIcon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ProgressBar
android:id="@+id/savingProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/error"
app:tint="?attr/colorOnSecondaryContainer"
android:src="@drawable/error"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/profile_saved" />
<ImageView
android:id="@+id/done"
android:src="@drawable/check_circle_24"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/profile_saved" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/progressText"
tools:text="@string/fetching_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressIcon"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -116,7 +116,6 @@
android:layout_height="wrap_content"
android:text="@string/edit_profile"
android:visibility="gone"
app:icon="@drawable/ic_baseline_open_in_browser_24"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"

View File

@ -11,7 +11,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:lineSpacingExtra="8sp"
android:text="Use dynamic color from your system"
android:text="@string/use_dynamic_color"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -310,4 +310,16 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<item quantity="other">%d replies</item>
</plurals>
<string name="save">Save</string>
<string name="use_dynamic_color">Use dynamic color from your system</string>
<string name="more_profile_settings">More profile settings</string>
<string name="private_account">Private Account</string>
<string name="private_account_explanation">When your account is private, only people you approve can see your photos and videos on pixelfed. Your existing followers won\'t be affected.</string>
<string name="your_bio">Your bio</string>
<string name="your_name">Your Name</string>
<string name="profile_save_changes">You did not save your changes. Exit?</string>
<string name="fetching_profile">Fetching your profile...</string>
<string name="saving_profile">Saving your profile</string>
<string name="profile_saved">Changes saved!</string>
<string name="error_profile">Something went wrong. Tap to retry</string>
<string name="change_profile_picture">Change your profile picture</string>
</resources>

View File

@ -8331,6 +8331,14 @@
<sha256 value="cced369639daee814671c2ff1b9d0f8acfbf598ce905487cb86feba54c9e8df1" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-android-extensions-runtime" version="1.7.20">
<artifact name="kotlin-android-extensions-runtime-1.7.20.jar">
<sha256 value="dc236f881dbe6c91e720f8b54df54427a0daf9a95ae6cf9a5c649a644afb234f" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-android-extensions-runtime-1.7.20.pom">
<sha256 value="3b076f3ad30172479605d09688544690aea4e997362fee6f2d6b1a765214917f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-annotation-processing-gradle" version="1.6.10">
<artifact name="kotlin-annotation-processing-gradle-1.6.10.jar">
<sha256 value="a0fb6d2cc2ebee6258384d1501533dfeab2e41385ac0668dc2cac2e4dd5a377c" origin="Generated by Gradle"/>
@ -8756,6 +8764,22 @@
<sha256 value="470d4d0badde58da624747a9ea16432bffa8ac8de81effea7a1408ec74902705" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-parcelize-compiler" version="1.7.20">
<artifact name="kotlin-parcelize-compiler-1.7.20.jar">
<sha256 value="d13f98e935257b8608b93db874b4b2c7c5a0e7fe5d785f2061c1830ad17b3f0d" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-parcelize-compiler-1.7.20.pom">
<sha256 value="4a941647ba7fc045fbadf94642bcedf25f10f669130386efd8b66e23bbd89e60" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-parcelize-runtime" version="1.7.20">
<artifact name="kotlin-parcelize-runtime-1.7.20.jar">
<sha256 value="da8d81fc588612d4b7282c35a209dbb3b974a3bc0fa5330a5db57b9583570ebd" origin="Generated by Gradle"/>
</artifact>
<artifact name="kotlin-parcelize-runtime-1.7.20.pom">
<sha256 value="2c4ea1b17d2b63dde60e32191a512adb3c7c35da033502b8dbc0ba7ae0cf140c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin" name="kotlin-project-model" version="1.6.10">
<artifact name="kotlin-project-model-1.6.10.jar">
<sha256 value="8d9f5e8e5402a05fb8c20447a56b697dc0cf73ea5c55534a9ee6a463920869c9" origin="Generated by Gradle"/>