Merge branch 'profileEdit' into 'master'

Edit profile in-app

See merge request pixeldroid/PixelDroid!434
This commit is contained in:
Matthieu 2022-10-30 19:52:34 +00:00
commit 2d712ed395
17 changed files with 758 additions and 12 deletions

View File

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

View File

@ -31,7 +31,9 @@
android:name=".posts.AlbumActivity" android:name=".posts.AlbumActivity"
android:exported="false" android:exported="false"
android:theme="@style/AppTheme.ActionBar.Transparent"/> android:theme="@style/AppTheme.ActionBar.Transparent"/>
<activity
android:name=".profile.EditProfileActivity"
android:exported="false"/>
<activity <activity
android:name=".posts.MediaViewerActivity" android:name=".posts.MediaViewerActivity"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"

View File

@ -0,0 +1,156 @@
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.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() {
private lateinit var model: EditProfileViewModel
private lateinit var binding: ActivityEditProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.edit_profile)
val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) }
model = _model
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
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){
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()
}
}
// 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())
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.edit_profile_menu, menu)
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.sendProfile()
return true
}
}
return super.onOptionsItemSelected(item)
}
}

View File

@ -0,0 +1,319 @@
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
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) {
@Inject
lateinit var apiHolder: PixelfedAPIHolder
private val _uiState = MutableStateFlow(EditProfileActivityUiState())
val uiState: StateFlow<EditProfileActivityUiState> = _uiState
var oldProfile: Account? = null
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
try {
val profile = api.verifyCredentials()
if (oldProfile == null) oldProfile = profile
_uiState.update { currentUiState ->
currentUiState.copy(
name = oldProfile?.display_name,
bio = oldProfile?.source?.note,
profilePictureUri = oldProfile?.anyAvatar()?.toUri(),
privateAccount = oldProfile?.locked,
loadingProfile = 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 sendProfile() {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
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 {
val account = api.updateCredentials(
displayName = name,
note = bio,
locked = privateAccount,
)
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(
sendingProfile = false,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true
)
}
} catch (exception: HttpException) {
Log.e("TAG", exception.toString())
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = false,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true
)
}
} catch (exception: Exception) {
Log.e("TAG", exception.toString())
}
}
}
}
fun errorShown() {
_uiState.update { currentUiState ->
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,
val bio: String? = null,
val profilePictureUri: Uri?= null,
val privateAccount: Boolean? = null,
val loadingProfile: Boolean = true,
val profileLoaded: Boolean = false,
val sendingProfile: 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 {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java).newInstance(application)
}
}

View File

@ -228,12 +228,8 @@ class ProfileActivity : BaseThemedWithBarActivity() {
} }
private fun onClickEditButton() { private fun onClickEditButton() {
val url = "$domain/settings/home" val intent = Intent(this, EditProfileActivity::class.java)
ContextCompat.startActivity(this, intent, null)
if(!openUrl(url)) {
Snackbar.make(binding.root, getString(R.string.edit_link_failed),
Snackbar.LENGTH_LONG).show()
}
} }
private fun onClickFollowers(account: Account?) { private fun onClickFollowers(account: Account?) {

View File

@ -295,6 +295,20 @@ interface PixelfedAPI {
@Header("Authorization") authorization: String? = null @Header("Authorization") authorization: String? = null
): Account ): Account
//@Multipart
@PATCH("/api/v1/accounts/update_credentials")
suspend fun updateCredentials(
@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") @GET("/api/v1/accounts/{id}/statuses")
suspend fun accountPosts( suspend fun accountPosts(

View File

@ -1,5 +1,12 @@
package org.pixeldroid.app.utils.api.objects package org.pixeldroid.app.utils.api.objects
import java.io.Serializable import java.io.Serializable
import java.time.Instant
class Field: Serializable data class Field(
//Required attributes
val name: String?,
val value: String?,
//Optional attributes
val verified_at: Instant?
): Serializable

View File

@ -2,4 +2,16 @@ package org.pixeldroid.app.utils.api.objects
import java.io.Serializable import java.io.Serializable
class Source: Serializable data class Source(
val note: String?,
val fields: List<Field>?,
//Nullable attributes
val privacy: Privacy?,
val sensitive: Boolean?,
val language: String?, //ISO 639-1 language two-letter code
val follow_requests_count: Int?,
): Serializable {
enum class Privacy: Serializable {
public, unlisted, private, direct
}
}

View File

@ -8,6 +8,7 @@ import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import dagger.Component import dagger.Component
import org.pixeldroid.app.postCreation.PostCreationViewModel import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
import javax.inject.Singleton import javax.inject.Singleton
@ -20,6 +21,7 @@ interface ApplicationComponent {
fun inject(feedFragment: BaseFragment) fun inject(feedFragment: BaseFragment)
fun inject(notificationsWorker: NotificationsWorker) fun inject(notificationsWorker: NotificationsWorker)
fun inject(postCreationViewModel: PostCreationViewModel) fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel)
val context: Context? val context: Context?
val application: Application? val application: Application?

View File

@ -0,0 +1,5 @@
<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="?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

@ -0,0 +1,184 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/profilePic"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
app:layout_constraintBottom_toTopOf="@+id/textInputLayoutName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars"
android:contentDescription="@string/profile_picture" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayoutName"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profilePic">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/nameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/your_name"
android:ems="10"
android:imeOptions="actionDone" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayoutBio"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutName">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bioEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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_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

@ -33,8 +33,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:hint="@string/domain_of_your_instance" android:hint="@string/domain_of_your_instance"
android:visibility="gone"
app:errorEnabled="true" app:errorEnabled="true"
android:visibility="gone"> tools:visibility="visible">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/editText" android:id="@+id/editText"

View File

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

View File

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

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_apply"
android:orderInCategory="100"
android:title="@string/save"
android:icon="@drawable/done"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -309,4 +309,17 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<item quantity="one">%d reply</item> <item quantity="one">%d reply</item>
<item quantity="other">%d replies</item> <item quantity="other">%d replies</item>
</plurals> </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> </resources>

View File

@ -8331,6 +8331,14 @@
<sha256 value="cced369639daee814671c2ff1b9d0f8acfbf598ce905487cb86feba54c9e8df1" origin="Generated by Gradle"/> <sha256 value="cced369639daee814671c2ff1b9d0f8acfbf598ce905487cb86feba54c9e8df1" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="org.jetbrains.kotlin" name="kotlin-annotation-processing-gradle" version="1.6.10">
<artifact name="kotlin-annotation-processing-gradle-1.6.10.jar"> <artifact name="kotlin-annotation-processing-gradle-1.6.10.jar">
<sha256 value="a0fb6d2cc2ebee6258384d1501533dfeab2e41385ac0668dc2cac2e4dd5a377c" origin="Generated by Gradle"/> <sha256 value="a0fb6d2cc2ebee6258384d1501533dfeab2e41385ac0668dc2cac2e4dd5a377c" origin="Generated by Gradle"/>
@ -8756,6 +8764,22 @@
<sha256 value="470d4d0badde58da624747a9ea16432bffa8ac8de81effea7a1408ec74902705" origin="Generated by Gradle"/> <sha256 value="470d4d0badde58da624747a9ea16432bffa8ac8de81effea7a1408ec74902705" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </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"> <component group="org.jetbrains.kotlin" name="kotlin-project-model" version="1.6.10">
<artifact name="kotlin-project-model-1.6.10.jar"> <artifact name="kotlin-project-model-1.6.10.jar">
<sha256 value="8d9f5e8e5402a05fb8c20447a56b697dc0cf73ea5c55534a9ee6a463920869c9" origin="Generated by Gradle"/> <sha256 value="8d9f5e8e5402a05fb8c20447a56b697dc0cf73ea5c55534a9ee6a463920869c9" origin="Generated by Gradle"/>