add possibility to change profile fields, refactor (#751)
* refactor EditProfileActivity, add profile fields * preserve transparency when cropping profile images * dont validate profile fields on client side * revert unintentional change in card_frame_dark.xml * improve activity_edit_profile layout for tablets * Revert "improve activity_edit_profile layout for tablets" This reverts commit 20ff3d167c39b15566e017108b33fe58690a8482. * improve activity_edit_profile layout for tablets * fix bug in EditProfileActivity, add snackbar * improve EditProfileActivity code * use events instead of shared prefs to communicate profile update
This commit is contained in:
parent
418c76d677
commit
f022944e90
16 changed files with 727 additions and 395 deletions
|
@ -16,7 +16,6 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.arch.lifecycle.Observer
|
||||
import android.arch.lifecycle.ViewModelProviders
|
||||
|
@ -376,7 +375,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
|
|||
accountFollowButton.setOnClickListener { _ ->
|
||||
if (isSelf) {
|
||||
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
|
||||
startActivityForResult(intent, EDIT_ACCOUNT)
|
||||
startActivity(intent)
|
||||
return@setOnClickListener
|
||||
}
|
||||
when (followState) {
|
||||
|
@ -395,15 +394,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
|
|||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
//reload account when returning from EditProfileActivity
|
||||
if(requestCode == EDIT_ACCOUNT && resultCode == Activity.RESULT_OK) {
|
||||
viewModel.obtainAccount(accountId, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putString(KEY_ACCOUNT_ID, accountId)
|
||||
super.onSaveInstanceState(outState)
|
||||
|
@ -610,8 +600,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
|
|||
|
||||
companion object {
|
||||
|
||||
private const val EDIT_ACCOUNT = 1457
|
||||
|
||||
private const val KEY_ACCOUNT_ID = "id"
|
||||
private val argbEvaluator = ArgbEvaluator()
|
||||
|
||||
|
|
|
@ -17,72 +17,59 @@ package com.keylesspalace.tusky
|
|||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ContentResolver
|
||||
import android.arch.lifecycle.LiveData
|
||||
import android.arch.lifecycle.Observer
|
||||
import android.arch.lifecycle.ViewModelProviders
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.Log
|
||||
import android.support.v4.widget.TextViewCompat
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.IOUtils
|
||||
import com.keylesspalace.tusky.util.MediaUtils
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.theartofdev.edmodo.cropper.CropImage
|
||||
import kotlinx.android.synthetic.main.activity_edit_profile.*
|
||||
import kotlinx.android.synthetic.main.toolbar_basic.*
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "EditProfileActivity"
|
||||
|
||||
private const val HEADER_FILE_NAME = "header.png"
|
||||
private const val AVATAR_FILE_NAME = "avatar.png"
|
||||
|
||||
private const val KEY_OLD_DISPLAY_NAME = "OLD_DISPLAY_NAME"
|
||||
private const val KEY_OLD_NOTE = "OLD_NOTE"
|
||||
private const val KEY_OLD_LOCKED = "OLD_LOCKED"
|
||||
private const val KEY_IS_SAVING = "IS_SAVING"
|
||||
private const val KEY_CURRENTLY_PICKING = "CURRENTLY_PICKING"
|
||||
private const val KEY_AVATAR_CHANGED = "AVATAR_CHANGED"
|
||||
private const val KEY_HEADER_CHANGED = "HEADER_CHANGED"
|
||||
|
||||
private const val AVATAR_PICK_RESULT = 1
|
||||
private const val HEADER_PICK_RESULT = 2
|
||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||
private const val AVATAR_SIZE = 400
|
||||
private const val HEADER_WIDTH = 700
|
||||
private const val HEADER_HEIGHT = 335
|
||||
|
||||
class EditProfileActivity : BaseActivity(), Injectable {
|
||||
|
||||
private var oldDisplayName: String? = null
|
||||
private var oldNote: String? = null
|
||||
private var oldLocked: Boolean = false
|
||||
private var isSaving: Boolean = false
|
||||
private var currentlyPicking: PickType = PickType.NOTHING
|
||||
private var avatarChanged: Boolean = false
|
||||
private var headerChanged: Boolean = false
|
||||
companion object {
|
||||
const val AVATAR_SIZE = 400
|
||||
const val HEADER_WIDTH = 700
|
||||
const val HEADER_HEIGHT = 335
|
||||
|
||||
private const val AVATAR_PICK_RESULT = 1
|
||||
private const val HEADER_PICK_RESULT = 2
|
||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||
private const val MAX_ACCOUNT_FIELDS = 4
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var mastodonApi: MastodonApi
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private lateinit var viewModel: EditProfileViewModel
|
||||
|
||||
private var currentlyPicking: PickType = PickType.NOTHING
|
||||
|
||||
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
||||
|
||||
private enum class PickType {
|
||||
NOTHING,
|
||||
|
@ -94,93 +81,127 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_edit_profile)
|
||||
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory)[EditProfileViewModel::class.java]
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.run {
|
||||
setTitle(R.string.title_edit_profile)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
savedInstanceState?.let {
|
||||
oldDisplayName = it.getString(KEY_OLD_DISPLAY_NAME)
|
||||
oldNote = it.getString(KEY_OLD_NOTE)
|
||||
oldLocked = it.getBoolean(KEY_OLD_LOCKED)
|
||||
isSaving = it.getBoolean(KEY_IS_SAVING)
|
||||
currentlyPicking = it.getSerializable(KEY_CURRENTLY_PICKING) as PickType
|
||||
avatarChanged = it.getBoolean(KEY_AVATAR_CHANGED)
|
||||
headerChanged = it.getBoolean(KEY_HEADER_CHANGED)
|
||||
|
||||
if (avatarChanged) {
|
||||
val avatar = BitmapFactory.decodeFile(getCacheFileForName(AVATAR_FILE_NAME).absolutePath)
|
||||
avatarPreview.setImageBitmap(avatar)
|
||||
}
|
||||
if (headerChanged) {
|
||||
val header = BitmapFactory.decodeFile(getCacheFileForName(HEADER_FILE_NAME).absolutePath)
|
||||
headerPreview.setImageBitmap(header)
|
||||
}
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) }
|
||||
headerButton.setOnClickListener { onMediaPick(PickType.HEADER) }
|
||||
|
||||
avatarPreview.setOnClickListener {
|
||||
avatarPreview.setImageBitmap(null)
|
||||
avatarPreview.visibility = View.INVISIBLE
|
||||
}
|
||||
headerPreview.setOnClickListener {
|
||||
headerPreview.setImageBitmap(null)
|
||||
headerPreview.visibility = View.INVISIBLE
|
||||
}
|
||||
fieldList.layoutManager = LinearLayoutManager(this)
|
||||
fieldList.adapter = accountFieldEditAdapter
|
||||
|
||||
mastodonApi.accountVerifyCredentials().enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
||||
if (!response.isSuccessful) {
|
||||
onAccountVerifyCredentialsFailed()
|
||||
return
|
||||
}
|
||||
val me = response.body()
|
||||
oldDisplayName = me!!.displayName
|
||||
oldNote = me.source?.note
|
||||
oldLocked = me.locked
|
||||
val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).sizeDp(12).color(Color.WHITE)
|
||||
|
||||
displayNameEditText.setText(oldDisplayName)
|
||||
noteEditText.setText(oldNote)
|
||||
lockedCheckBox.isChecked = oldLocked
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(addFieldButton, plusDrawable, null, null, null)
|
||||
|
||||
if (!avatarChanged) {
|
||||
Picasso.with(avatarPreview.context)
|
||||
.load(me.avatar)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.into(avatarPreview)
|
||||
}
|
||||
if (!headerChanged) {
|
||||
Picasso.with(headerPreview.context)
|
||||
.load(me.header)
|
||||
.into(headerPreview)
|
||||
}
|
||||
addFieldButton.setOnClickListener {
|
||||
accountFieldEditAdapter.addField()
|
||||
if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
|
||||
it.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
onAccountVerifyCredentialsFailed()
|
||||
scrollView.post{
|
||||
scrollView.smoothScrollTo(0, it.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.obtainProfile()
|
||||
|
||||
viewModel.profileData.observe(this, Observer<Resource<Account>> { profileRes ->
|
||||
when (profileRes) {
|
||||
is Success -> {
|
||||
val me = profileRes.data
|
||||
if (me != null) {
|
||||
|
||||
displayNameEditText.setText(me.displayName)
|
||||
noteEditText.setText(me.source?.note)
|
||||
lockedCheckBox.isChecked = me.locked
|
||||
|
||||
accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList())
|
||||
|
||||
if(viewModel.avatarData.value == null) {
|
||||
Picasso.with(this)
|
||||
.load(me.avatar)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.into(avatarPreview)
|
||||
}
|
||||
|
||||
if(viewModel.headerData.value == null) {
|
||||
Picasso.with(this)
|
||||
.load(me.header)
|
||||
.into(headerPreview)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
is Error -> {
|
||||
val snackbar = Snackbar.make(avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG);
|
||||
snackbar.setAction(R.string.action_retry) {
|
||||
viewModel.obtainProfile()
|
||||
}
|
||||
snackbar.show()
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar)
|
||||
observeImage(viewModel.headerData, headerPreview, headerProgressBar)
|
||||
|
||||
viewModel.saveData.observe(this, Observer<Resource<Nothing>> {
|
||||
when(it) {
|
||||
is Success -> {
|
||||
finish()
|
||||
}
|
||||
is Loading -> {
|
||||
saveProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is Error -> {
|
||||
onSaveFailure(it.errorMessage)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.run {
|
||||
putString(KEY_OLD_DISPLAY_NAME, oldDisplayName)
|
||||
putString(KEY_OLD_NOTE, oldNote)
|
||||
putBoolean(KEY_OLD_LOCKED, oldLocked)
|
||||
putBoolean(KEY_IS_SAVING, isSaving)
|
||||
putSerializable(KEY_CURRENTLY_PICKING, currentlyPicking)
|
||||
putBoolean(KEY_AVATAR_CHANGED, avatarChanged)
|
||||
putBoolean(KEY_HEADER_CHANGED, headerChanged)
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if(!isFinishing) {
|
||||
viewModel.updateProfile(displayNameEditText.text.toString(),
|
||||
noteEditText.text.toString(),
|
||||
lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData())
|
||||
}
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun onAccountVerifyCredentialsFailed() {
|
||||
Log.e(TAG, "The account failed to load.")
|
||||
private fun observeImage(liveData: LiveData<Resource<Bitmap>>, imageView: ImageView, progressBar: View) {
|
||||
liveData.observe(this, Observer<Resource<Bitmap>> {
|
||||
|
||||
when (it) {
|
||||
is Success -> {
|
||||
imageView.setImageBitmap(it.data)
|
||||
imageView.show()
|
||||
progressBar.hide()
|
||||
}
|
||||
is Loading -> {
|
||||
progressBar.show()
|
||||
}
|
||||
is Error -> {
|
||||
progressBar.hide()
|
||||
if(!it.consumed) {
|
||||
onResizeFailure()
|
||||
it.consumed = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onMediaPick(pickType: PickType) {
|
||||
|
@ -245,77 +266,20 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
private fun save() {
|
||||
if (isSaving || currentlyPicking != PickType.NOTHING) {
|
||||
return
|
||||
if (currentlyPicking != PickType.NOTHING) {
|
||||
return
|
||||
}
|
||||
|
||||
isSaving = true
|
||||
saveProgressBar.visibility = View.VISIBLE
|
||||
|
||||
val newDisplayName = displayNameEditText.text.toString()
|
||||
val displayName = if (oldDisplayName == newDisplayName) {
|
||||
null
|
||||
} else {
|
||||
RequestBody.create(MultipartBody.FORM, newDisplayName)
|
||||
}
|
||||
|
||||
val newNote = noteEditText.text.toString()
|
||||
val note = if (oldNote == newNote) {
|
||||
null
|
||||
} else {
|
||||
RequestBody.create(MultipartBody.FORM, newNote)
|
||||
}
|
||||
|
||||
val newLocked = lockedCheckBox.isChecked
|
||||
val locked = if (oldLocked == newLocked) {
|
||||
null
|
||||
} else {
|
||||
RequestBody.create(MultipartBody.FORM, newLocked.toString())
|
||||
}
|
||||
|
||||
val avatar = if (avatarChanged) {
|
||||
val avatarBody = RequestBody.create(MediaType.parse("image/png"), getCacheFileForName(AVATAR_FILE_NAME))
|
||||
MultipartBody.Part.createFormData("avatar", getFileName(), avatarBody)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val header = if (headerChanged) {
|
||||
val headerBody = RequestBody.create(MediaType.parse("image/png"), getCacheFileForName(HEADER_FILE_NAME))
|
||||
MultipartBody.Part.createFormData("header", getFileName(), headerBody)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (displayName == null && note == null && locked == null && avatar == null && header == null) {
|
||||
/** if nothing has changed, there is no need to make a network request */
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
mastodonApi.accountUpdateCredentials(displayName, note, locked, avatar, header).enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
||||
if (!response.isSuccessful) {
|
||||
onSaveFailure()
|
||||
return
|
||||
}
|
||||
privatePreferences.edit()
|
||||
.putBoolean("refreshProfileHeader", true)
|
||||
.apply()
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
onSaveFailure()
|
||||
}
|
||||
})
|
||||
viewModel.save(displayNameEditText.text.toString(),
|
||||
noteEditText.text.toString(),
|
||||
lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData(),
|
||||
this)
|
||||
}
|
||||
|
||||
private fun onSaveFailure() {
|
||||
isSaving = false
|
||||
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
||||
private fun onSaveFailure(msg: String?) {
|
||||
val errorMsg = msg ?: getString(R.string.error_media_upload_sending)
|
||||
Snackbar.make(avatarButton, errorMsg, Snackbar.LENGTH_LONG).show()
|
||||
saveProgressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
@ -350,6 +314,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
CropImage.activity(data.data)
|
||||
.setInitialCropWindowPaddingRatio(0f)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
||||
.start(this)
|
||||
} else {
|
||||
|
@ -360,6 +325,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
CropImage.activity(data.data)
|
||||
.setInitialCropWindowPaddingRatio(0f)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
||||
.start(this)
|
||||
} else {
|
||||
|
@ -379,50 +345,21 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
private fun beginResize(uri: Uri) {
|
||||
beginMediaPicking()
|
||||
val width: Int
|
||||
val height: Int
|
||||
val cacheFile: File
|
||||
|
||||
when (currentlyPicking) {
|
||||
EditProfileActivity.PickType.AVATAR -> {
|
||||
width = AVATAR_SIZE
|
||||
height = AVATAR_SIZE
|
||||
cacheFile = getCacheFileForName(AVATAR_FILE_NAME)
|
||||
viewModel.newAvatar(uri, this)
|
||||
}
|
||||
EditProfileActivity.PickType.HEADER -> {
|
||||
width = HEADER_WIDTH
|
||||
height = HEADER_HEIGHT
|
||||
cacheFile = getCacheFileForName(HEADER_FILE_NAME)
|
||||
viewModel.newHeader(uri, this)
|
||||
}
|
||||
else -> {
|
||||
throw AssertionError("PickType not set.")
|
||||
}
|
||||
}
|
||||
ResizeImageTask(contentResolver, width, height, cacheFile, object : ResizeImageTask.Listener {
|
||||
override fun onSuccess(resizedImage: Bitmap?) {
|
||||
val pickType = currentlyPicking
|
||||
endMediaPicking()
|
||||
when (pickType) {
|
||||
EditProfileActivity.PickType.AVATAR -> {
|
||||
avatarPreview.setImageBitmap(resizedImage)
|
||||
avatarPreview.visibility = View.VISIBLE
|
||||
avatarButton.setImageResource(R.drawable.ic_add_a_photo_32dp)
|
||||
avatarChanged = true
|
||||
}
|
||||
EditProfileActivity.PickType.HEADER -> {
|
||||
headerPreview.setImageBitmap(resizedImage)
|
||||
headerPreview.visibility = View.VISIBLE
|
||||
headerButton.setImageResource(R.drawable.ic_add_a_photo_32dp)
|
||||
headerChanged = true
|
||||
}
|
||||
EditProfileActivity.PickType.NOTHING -> { /* do nothing */ }
|
||||
|
||||
}
|
||||
}
|
||||
currentlyPicking = PickType.NOTHING
|
||||
|
||||
override fun onFailure() {
|
||||
onResizeFailure()
|
||||
}
|
||||
}).execute(uri)
|
||||
}
|
||||
|
||||
private fun onResizeFailure() {
|
||||
|
@ -430,80 +367,4 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
endMediaPicking()
|
||||
}
|
||||
|
||||
private fun getCacheFileForName(filename: String): File {
|
||||
return File(cacheDir, filename)
|
||||
}
|
||||
|
||||
private fun getFileName(): String {
|
||||
return java.lang.Long.toHexString(Random().nextLong())
|
||||
}
|
||||
|
||||
private class ResizeImageTask(private val contentResolver: ContentResolver,
|
||||
private val resizeWidth: Int,
|
||||
private val resizeHeight: Int,
|
||||
private val cacheFile: File,
|
||||
private val listener: Listener) : AsyncTask<Uri, Void, Boolean>() {
|
||||
private var resultBitmap: Bitmap? = null
|
||||
|
||||
override fun doInBackground(vararg uris: Uri): Boolean? {
|
||||
val uri = uris[0]
|
||||
|
||||
val sourceBitmap = MediaUtils.getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight)
|
||||
|
||||
if (sourceBitmap == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
//dont upscale image if its smaller than the desired size
|
||||
val bitmap =
|
||||
if (sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) {
|
||||
sourceBitmap
|
||||
} else {
|
||||
Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true)
|
||||
}
|
||||
|
||||
resultBitmap = bitmap
|
||||
|
||||
if (!saveBitmapToFile(bitmap, cacheFile)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isCancelled) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPostExecute(successful: Boolean) {
|
||||
if (successful) {
|
||||
listener.onSuccess(resultBitmap)
|
||||
} else {
|
||||
listener.onFailure()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveBitmapToFile(bitmap: Bitmap, file: File): Boolean {
|
||||
|
||||
val outputStream: OutputStream
|
||||
|
||||
try {
|
||||
outputStream = FileOutputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.w(TAG, Log.getStackTraceString(e))
|
||||
return false
|
||||
}
|
||||
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
IOUtils.closeQuietly(outputStream)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
internal interface Listener {
|
||||
fun onSuccess(resizedImage: Bitmap?)
|
||||
fun onFailure()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.arch.lifecycle.Lifecycle;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
|
@ -36,6 +36,8 @@ import android.view.KeyEvent;
|
|||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||
|
@ -67,10 +69,14 @@ import javax.inject.Inject;
|
|||
import dagger.android.AndroidInjector;
|
||||
import dagger.android.DispatchingAndroidInjector;
|
||||
import dagger.android.support.HasSupportFragmentInjector;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||
|
||||
public final class MainActivity extends BottomSheetActivity implements ActionButtonActivity,
|
||||
HasSupportFragmentInjector {
|
||||
|
||||
|
@ -90,6 +96,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
|
||||
@Inject
|
||||
public DispatchingAndroidInjector<Fragment> fragmentInjector;
|
||||
@Inject
|
||||
public EventHub eventHub;
|
||||
|
||||
private FloatingActionButton composeButton;
|
||||
private AccountHeader headerResult;
|
||||
|
@ -211,6 +219,15 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
disablePushNotifications();
|
||||
}
|
||||
|
||||
eventHub.getEvents()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||
.subscribe(event -> {
|
||||
if (event instanceof ProfileEditedEvent) {
|
||||
onFetchUserInfoSuccess(((ProfileEditedEvent) event).getNewProfileData());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -219,16 +236,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
|
||||
NotificationHelper.clearNotificationsForActiveAccount(this, accountManager);
|
||||
|
||||
/* After editing a profile, the profile header in the navigation drawer needs to be
|
||||
* refreshed */
|
||||
SharedPreferences preferences = getPrivatePreferences();
|
||||
if (preferences.getBoolean("refreshProfileHeader", false)) {
|
||||
fetchUserInfo();
|
||||
preferences.edit()
|
||||
.putBoolean("refreshProfileHeader", false)
|
||||
.apply();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.StringField
|
||||
import kotlinx.android.synthetic.main.item_edit_field.view.*
|
||||
|
||||
class AccountFieldEditAdapter : RecyclerView.Adapter<AccountFieldEditAdapter.ViewHolder>() {
|
||||
|
||||
private val fieldData = mutableListOf<MutableStringPair>()
|
||||
|
||||
fun setFields(fields: List<StringField>) {
|
||||
fieldData.clear()
|
||||
|
||||
fields.forEach { field ->
|
||||
fieldData.add(MutableStringPair(field.name, field.value))
|
||||
}
|
||||
if(fieldData.isEmpty()) {
|
||||
fieldData.add(MutableStringPair("", ""))
|
||||
}
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun getFieldData(): List<StringField> {
|
||||
return fieldData.map {
|
||||
StringField(it.first, it.second)
|
||||
}
|
||||
}
|
||||
|
||||
fun addField() {
|
||||
fieldData.add(MutableStringPair("", ""))
|
||||
notifyItemInserted(fieldData.size - 1)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = fieldData.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountFieldEditAdapter.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_edit_field, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: AccountFieldEditAdapter.ViewHolder, position: Int) {
|
||||
viewHolder.nameTextView.setText(fieldData[position].first)
|
||||
viewHolder.valueTextView.setText(fieldData[position].second)
|
||||
|
||||
viewHolder.nameTextView.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(newText: Editable) {
|
||||
fieldData[viewHolder.adapterPosition].first = newText.toString()
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
viewHolder.valueTextView.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(newText: Editable) {
|
||||
fieldData[viewHolder.adapterPosition].second = newText.toString()
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
class ViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView) {
|
||||
val nameTextView: EditText = rootView.accountFieldName
|
||||
val valueTextView: EditText = rootView.accountFieldValue
|
||||
}
|
||||
|
||||
class MutableStringPair (var first: String, var second: String)
|
||||
|
||||
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.keylesspalace.tusky.appstore
|
||||
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
||||
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
|
||||
|
@ -8,4 +9,5 @@ data class UnfollowEvent(val accountId: String) : Dispatchable
|
|||
data class BlockEvent(val accountId: String) : Dispatchable
|
||||
data class MuteEvent(val accountId: String) : Dispatchable
|
||||
data class StatusDeletedEvent(val statusId: String) : Dispatchable
|
||||
data class StatusComposedEvent(val status: Status) : Dispatchable
|
||||
data class StatusComposedEvent(val status: Status) : Dispatchable
|
||||
data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
|
@ -5,6 +5,7 @@ package com.keylesspalace.tusky.di
|
|||
import android.arch.lifecycle.ViewModel
|
||||
import android.arch.lifecycle.ViewModelProvider
|
||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
import dagger.Binds
|
||||
import dagger.MapKey
|
||||
import dagger.Module
|
||||
|
@ -16,7 +17,7 @@ import kotlin.reflect.KClass
|
|||
|
||||
@Singleton
|
||||
class ViewModelFactory @Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
|
||||
}
|
||||
|
||||
|
@ -36,5 +37,10 @@ abstract class ViewModelModule {
|
|||
@ViewModelKey(AccountViewModel::class)
|
||||
internal abstract fun accountViewModel(viewModel: AccountViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(EditProfileViewModel::class)
|
||||
internal abstract fun editProfileViewModel(viewModel: EditProfileViewModel): ViewModel
|
||||
|
||||
//Add more ViewModels here
|
||||
}
|
|
@ -70,15 +70,22 @@ data class Account(
|
|||
data class AccountSource(
|
||||
val privacy: Status.Visibility,
|
||||
val sensitive: Boolean,
|
||||
val note: String
|
||||
val note: String,
|
||||
val fields: List<StringField>?
|
||||
): Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Field (
|
||||
val name:String,
|
||||
val name: String,
|
||||
val value: @WriteWith<SpannedParceler>() Spanned
|
||||
): Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class StringField (
|
||||
val name: String,
|
||||
val value: String
|
||||
): Parcelable
|
||||
|
||||
object SpannedParceler : Parceler<Spanned> {
|
||||
override fun create(parcel: Parcel): Spanned = HtmlUtils.fromHtml(parcel.readString())
|
||||
|
||||
|
|
|
@ -166,13 +166,22 @@ public interface MastodonApi {
|
|||
@Nullable @Part(value="note") RequestBody note,
|
||||
@Nullable @Part(value="locked") RequestBody locked,
|
||||
@Nullable @Part MultipartBody.Part avatar,
|
||||
@Nullable @Part MultipartBody.Part header);
|
||||
@Nullable @Part MultipartBody.Part header,
|
||||
@Nullable @Part(value="fields_attributes[0][name]") RequestBody fieldName0,
|
||||
@Nullable @Part(value="fields_attributes[0][value]") RequestBody fieldValue0,
|
||||
@Nullable @Part(value="fields_attributes[1][name]") RequestBody fieldName1,
|
||||
@Nullable @Part(value="fields_attributes[1][value]") RequestBody fieldValue1,
|
||||
@Nullable @Part(value="fields_attributes[2][name]") RequestBody fieldName2,
|
||||
@Nullable @Part(value="fields_attributes[2][value]") RequestBody fieldValue2,
|
||||
@Nullable @Part(value="fields_attributes[3][name]") RequestBody fieldName3,
|
||||
@Nullable @Part(value="fields_attributes[3][value]") RequestBody fieldValue3);
|
||||
|
||||
@GET("api/v1/accounts/search")
|
||||
Call<List<Account>> searchAccounts(
|
||||
@Query("q") String q,
|
||||
@Query("resolve") Boolean resolve,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/accounts/{id}")
|
||||
Call<Account> account(@Path("id") String accountId);
|
||||
|
||||
|
|
|
@ -6,4 +6,7 @@ class Loading<T> (override val data: T? = null) : Resource<T>(data)
|
|||
|
||||
class Success<T> (override val data: T? = null) : Resource<T>(data)
|
||||
|
||||
class Error<T> (override val data: T? = null, val errorMessage: String? = null): Resource<T>(data)
|
||||
class Error<T> (override val data: T? = null,
|
||||
val errorMessage: String? = null,
|
||||
var consumed: Boolean = false
|
||||
): Resource<T>(data)
|
|
@ -2,10 +2,7 @@ package com.keylesspalace.tusky.viewmodel
|
|||
|
||||
import android.arch.lifecycle.MutableLiveData
|
||||
import android.arch.lifecycle.ViewModel
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
|
@ -13,6 +10,7 @@ import com.keylesspalace.tusky.util.Error
|
|||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import io.reactivex.disposables.Disposable
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
@ -27,6 +25,12 @@ class AccountViewModel @Inject constructor(
|
|||
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
||||
|
||||
private val callList: MutableList<Call<*>> = mutableListOf()
|
||||
private val disposable: Disposable = eventHub.events
|
||||
.subscribe { event ->
|
||||
if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) {
|
||||
accountData.postValue(Success(event.newProfileData))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun obtainAccount(accountId: String, reload: Boolean = false) {
|
||||
|
@ -182,6 +186,7 @@ class AccountViewModel @Inject constructor(
|
|||
callList.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
enum class RelationShipAction {
|
||||
|
|
|
@ -0,0 +1,271 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.viewmodel
|
||||
|
||||
import android.arch.lifecycle.MutableLiveData
|
||||
import android.arch.lifecycle.ViewModel
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.keylesspalace.tusky.EditProfileActivity.Companion.AVATAR_SIZE
|
||||
import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_HEIGHT
|
||||
import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_WIDTH
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.StringField
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import org.json.JSONObject
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HEADER_FILE_NAME = "header.png"
|
||||
private const val AVATAR_FILE_NAME = "avatar.png"
|
||||
|
||||
private const val TAG = "EditProfileViewModel"
|
||||
|
||||
class EditProfileViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
): ViewModel() {
|
||||
|
||||
val profileData = MutableLiveData<Resource<Account>>()
|
||||
val avatarData = MutableLiveData<Resource<Bitmap>>()
|
||||
val headerData = MutableLiveData<Resource<Bitmap>>()
|
||||
val saveData = MutableLiveData<Resource<Nothing>>()
|
||||
|
||||
private var oldProfileData: Account? = null
|
||||
|
||||
private val callList: MutableList<Call<*>> = mutableListOf()
|
||||
|
||||
fun obtainProfile() {
|
||||
if(profileData.value == null || profileData.value is Error) {
|
||||
|
||||
profileData.postValue(Loading())
|
||||
|
||||
val call = mastodonApi.accountVerifyCredentials()
|
||||
call.enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>,
|
||||
response: Response<Account>) {
|
||||
if (response.isSuccessful) {
|
||||
val profile = response.body()
|
||||
oldProfileData = profile
|
||||
profileData.postValue(Success(profile))
|
||||
} else {
|
||||
profileData.postValue(Error())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
profileData.postValue(Error())
|
||||
}
|
||||
})
|
||||
|
||||
callList.add(call)
|
||||
}
|
||||
}
|
||||
|
||||
fun newAvatar(uri: Uri, context: Context) {
|
||||
val cacheFile = getCacheFileForName(context, AVATAR_FILE_NAME)
|
||||
|
||||
resizeImage(uri, context, AVATAR_SIZE, AVATAR_SIZE, cacheFile, avatarData)
|
||||
}
|
||||
|
||||
fun newHeader(uri: Uri, context: Context) {
|
||||
val cacheFile = getCacheFileForName(context, HEADER_FILE_NAME)
|
||||
|
||||
resizeImage(uri, context, HEADER_WIDTH, HEADER_HEIGHT, cacheFile, headerData)
|
||||
}
|
||||
|
||||
private fun resizeImage(uri: Uri,
|
||||
context: Context,
|
||||
resizeWidth: Int,
|
||||
resizeHeight: Int,
|
||||
cacheFile: File,
|
||||
imageLiveData: MutableLiveData<Resource<Bitmap>>) {
|
||||
|
||||
Single.fromCallable {
|
||||
val contentResolver = context.contentResolver
|
||||
val sourceBitmap = MediaUtils.getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight)
|
||||
|
||||
if (sourceBitmap == null) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
//dont upscale image if its smaller than the desired size
|
||||
val bitmap =
|
||||
if (sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) {
|
||||
sourceBitmap
|
||||
} else {
|
||||
Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true)
|
||||
}
|
||||
|
||||
if (!saveBitmapToFile(bitmap, cacheFile)) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
bitmap
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
imageLiveData.postValue(Success(it))
|
||||
}, {
|
||||
imageLiveData.postValue(Error())
|
||||
})
|
||||
}
|
||||
|
||||
fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List<StringField>, context: Context) {
|
||||
|
||||
if(saveData.value is Loading || profileData.value !is Success) {
|
||||
return
|
||||
}
|
||||
|
||||
val displayName = if (oldProfileData?.displayName == newDisplayName) {
|
||||
null
|
||||
} else {
|
||||
RequestBody.create(MultipartBody.FORM, newDisplayName)
|
||||
}
|
||||
|
||||
val note = if (oldProfileData?.source?.note == newNote) {
|
||||
null
|
||||
} else {
|
||||
RequestBody.create(MultipartBody.FORM, newNote)
|
||||
}
|
||||
|
||||
val locked = if (oldProfileData?.locked == newLocked) {
|
||||
null
|
||||
} else {
|
||||
RequestBody.create(MultipartBody.FORM, newLocked.toString())
|
||||
}
|
||||
|
||||
val avatar = if (avatarData.value is Success && avatarData.value?.data != null) {
|
||||
val avatarBody = RequestBody.create(MediaType.parse("image/png"), getCacheFileForName(context, AVATAR_FILE_NAME))
|
||||
MultipartBody.Part.createFormData("avatar", StringUtils.randomAlphanumericString(12), avatarBody)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val header = if (headerData.value is Success && headerData.value?.data != null) {
|
||||
val headerBody = RequestBody.create(MediaType.parse("image/png"), getCacheFileForName(context, HEADER_FILE_NAME))
|
||||
MultipartBody.Part.createFormData("header", StringUtils.randomAlphanumericString(12), headerBody)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// when one field changed, all have to be sent or they unchanged ones would get overridden
|
||||
val fieldsUnchanged = oldProfileData?.source?.fields == newFields
|
||||
val field1 = calculateFieldToUpdate(newFields.getOrNull(0), fieldsUnchanged)
|
||||
val field2 = calculateFieldToUpdate(newFields.getOrNull(1), fieldsUnchanged)
|
||||
val field3 = calculateFieldToUpdate(newFields.getOrNull(2), fieldsUnchanged)
|
||||
val field4 = calculateFieldToUpdate(newFields.getOrNull(3), fieldsUnchanged)
|
||||
|
||||
if (displayName == null && note == null && locked == null && avatar == null && header == null
|
||||
&& field1 == null && field2 == null && field3 == null && field4 == null) {
|
||||
/** if nothing has changed, there is no need to make a network request */
|
||||
saveData.postValue(Success())
|
||||
return
|
||||
}
|
||||
|
||||
mastodonApi.accountUpdateCredentials(displayName, note, locked, avatar, header,
|
||||
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
||||
).enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
||||
val newProfileData = response.body()
|
||||
if (!response.isSuccessful || newProfileData == null) {
|
||||
val errorResponse = response.errorBody()?.string()
|
||||
val errorMsg = if(!errorResponse.isNullOrBlank()) {
|
||||
JSONObject(errorResponse).optString("error", null)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
saveData.postValue(Error(errorMessage = errorMsg))
|
||||
return
|
||||
}
|
||||
saveData.postValue(Success())
|
||||
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
saveData.postValue(Error())
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// cache activity state for rotation change
|
||||
fun updateProfile(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List<StringField>) {
|
||||
if(profileData.value is Success) {
|
||||
val newProfileSource = profileData.value?.data?.source?.copy(note = newNote, fields = newFields)
|
||||
val newProfile = profileData.value?.data?.copy(displayName = newDisplayName,
|
||||
locked = newLocked, source = newProfileSource)
|
||||
|
||||
profileData.postValue(Success(newProfile))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun calculateFieldToUpdate(newField: StringField?, fieldsUnchanged: Boolean): Pair<RequestBody, RequestBody>? {
|
||||
if(fieldsUnchanged || newField == null || newField.name.isBlank()) {
|
||||
return null
|
||||
}
|
||||
return Pair(
|
||||
RequestBody.create(MultipartBody.FORM, newField.name),
|
||||
RequestBody.create(MultipartBody.FORM, newField.value)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCacheFileForName(context: Context, filename: String): File {
|
||||
return File(context.cacheDir, filename)
|
||||
}
|
||||
|
||||
private fun saveBitmapToFile(bitmap: Bitmap, file: File): Boolean {
|
||||
|
||||
val outputStream: OutputStream
|
||||
|
||||
try {
|
||||
outputStream = FileOutputStream(file)
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.w(TAG, Log.getStackTraceString(e))
|
||||
return false
|
||||
}
|
||||
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
IOUtils.closeQuietly(outputStream)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
callList.forEach {
|
||||
it.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
<include layout="@layout/toolbar_basic" />
|
||||
|
||||
<ScrollView
|
||||
<android.support.v4.widget.NestedScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
@ -47,98 +48,130 @@
|
|||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="@dimen/timeline_width"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="-40dp">
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="-40dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.keylesspalace.tusky.view.RoundedImageView
|
||||
android:id="@+id/avatarPreview"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/avatarButton"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:background="@drawable/round_button"
|
||||
android:contentDescription="@string/label_avatar"
|
||||
android:elevation="4dp"
|
||||
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/avatarProgressBar"
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
android:layout_marginStart="16dp">
|
||||
|
||||
</RelativeLayout>
|
||||
<com.keylesspalace.tusky.view.RoundedImageView
|
||||
android:id="@+id/avatarPreview"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/layout_edit_profile_display_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp">
|
||||
<ImageButton
|
||||
android:id="@+id/avatarButton"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:background="@drawable/round_button"
|
||||
android:contentDescription="@string/label_avatar"
|
||||
android:elevation="4dp"
|
||||
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/displayNameEditText"
|
||||
<ProgressBar
|
||||
android:id="@+id/avatarProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/displayNameEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:hint="@string/hint_display_name"
|
||||
android:importantForAutofill="no" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/noteEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:hint="@string/hint_note"
|
||||
android:importantForAutofill="no" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<android.support.v7.widget.AppCompatCheckBox
|
||||
android:id="@+id/lockedCheckBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:hint="@string/hint_display_name"
|
||||
android:importantForAutofill="no"
|
||||
android:maxLength="30" />
|
||||
android:layout_marginTop="30dp"
|
||||
android:paddingStart="8dp"
|
||||
android:text="@string/lock_account_label"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/layout_edit_profile_note"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp">
|
||||
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/noteEditText"
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:paddingStart="40dp"
|
||||
android:text="@string/lock_account_label_description"
|
||||
android:textSize="?attr/status_text_small" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="@string/profile_metadata_label"
|
||||
android:textSize="?attr/status_text_small" />
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/fieldList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:nestedScrollingEnabled="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/addFieldButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:hint="@string/hint_note"
|
||||
android:importantForAutofill="no"
|
||||
android:maxLength="160" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<android.support.v7.widget.AppCompatCheckBox
|
||||
android:id="@+id/lockedCheckBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:paddingStart="8dp"
|
||||
android:text="@string/lock_account_label"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:paddingStart="40dp"
|
||||
android:text="@string/lock_account_label_description"
|
||||
android:textSize="?attr/status_text_small" />
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/profile_metadata_add"
|
||||
android:textColor="#fff" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
</android.support.v4.widget.NestedScrollView>
|
||||
|
||||
<include layout="@layout/toolbar_shadow_shim" />
|
||||
|
||||
|
|
36
app/src/main/res/layout/item_edit_field.xml
Normal file
36
app/src/main/res/layout/item_edit_field.xml
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:colorBackground"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.text.emoji.widget.EmojiEditText
|
||||
android:id="@+id/accountFieldName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:hint="@string/profile_metadata_label_label" />
|
||||
|
||||
<android.support.text.emoji.widget.EmojiEditText
|
||||
android:id="@+id/accountFieldValue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:hint="@string/profile_metadata_content_label" />
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.v7.widget.CardView>
|
|
@ -6,7 +6,8 @@
|
|||
<item name="colorPrimary">@color/color_primary_dark</item>
|
||||
<item name="colorPrimaryDark">@color/color_primary_dark_dark</item>
|
||||
<item name="colorAccent">@color/color_accent_dark</item>
|
||||
<item name="colorButtonNormal">@color/button_dark</item>
|
||||
<item name="colorButtonNormal">@color/toolbar_background_dark</item>
|
||||
<item name="android:buttonStyle">@style/Widget.AppCompat.Button.Colored</item>
|
||||
|
||||
<item name="android:colorBackground">@color/color_primary_dark_dark</item>
|
||||
<item name="android:windowBackground">@color/window_background_dark</item>
|
||||
|
|
|
@ -340,5 +340,9 @@
|
|||
<string name="license_description">Tusky contains code and assets from the following open source projects:</string>
|
||||
<string name="license_apache_2">Licensed under the Apache License (copy below)</string>
|
||||
<string name="license_cc_by_4">CC-BY 4.0</string>
|
||||
<string name="profile_metadata_label">Profile metadata</string>
|
||||
<string name="profile_metadata_add">add data</string>
|
||||
<string name="profile_metadata_label_label">Label</string>
|
||||
<string name="profile_metadata_content_label">Content</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -55,7 +55,8 @@
|
|||
<item name="colorPrimary">@color/color_primary_light</item>
|
||||
<item name="colorPrimaryDark">@color/color_primary_dark_light</item>
|
||||
<item name="colorAccent">@color/color_accent_light</item>
|
||||
<item name="colorButtonNormal">@color/button_light</item>
|
||||
<item name="colorButtonNormal">@color/color_primary_dark_light</item>
|
||||
<item name="android:buttonStyle">@style/Widget.AppCompat.Button.Colored</item>
|
||||
|
||||
<item name="android:colorBackground">@color/color_background_light</item>
|
||||
<item name="android:windowBackground">@color/window_background_light</item>
|
||||
|
|
Loading…
Reference in a new issue