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,7 +16,6 @@
|
|||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.animation.ArgbEvaluator
|
import android.animation.ArgbEvaluator
|
||||||
import android.app.Activity
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.arch.lifecycle.Observer
|
import android.arch.lifecycle.Observer
|
||||||
import android.arch.lifecycle.ViewModelProviders
|
import android.arch.lifecycle.ViewModelProviders
|
||||||
@ -376,7 +375,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
|
|||||||
accountFollowButton.setOnClickListener { _ ->
|
accountFollowButton.setOnClickListener { _ ->
|
||||||
if (isSelf) {
|
if (isSelf) {
|
||||||
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
|
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
|
||||||
startActivityForResult(intent, EDIT_ACCOUNT)
|
startActivity(intent)
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
when (followState) {
|
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) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putString(KEY_ACCOUNT_ID, accountId)
|
outState.putString(KEY_ACCOUNT_ID, accountId)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
@ -610,8 +600,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val EDIT_ACCOUNT = 1457
|
|
||||||
|
|
||||||
private const val KEY_ACCOUNT_ID = "id"
|
private const val KEY_ACCOUNT_ID = "id"
|
||||||
private val argbEvaluator = ArgbEvaluator()
|
private val argbEvaluator = ArgbEvaluator()
|
||||||
|
|
||||||
|
@ -17,72 +17,59 @@ package com.keylesspalace.tusky
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.Activity
|
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.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.AsyncTask
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.design.widget.Snackbar
|
import android.support.design.widget.Snackbar
|
||||||
import android.support.v4.app.ActivityCompat
|
import android.support.v4.app.ActivityCompat
|
||||||
import android.support.v4.content.ContextCompat
|
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.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
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.Injectable
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.util.*
|
||||||
import com.keylesspalace.tusky.util.IOUtils
|
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||||
import com.keylesspalace.tusky.util.MediaUtils
|
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||||
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import com.theartofdev.edmodo.cropper.CropImage
|
import com.theartofdev.edmodo.cropper.CropImage
|
||||||
import kotlinx.android.synthetic.main.activity_edit_profile.*
|
import kotlinx.android.synthetic.main.activity_edit_profile.*
|
||||||
import kotlinx.android.synthetic.main.toolbar_basic.*
|
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
|
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 {
|
class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
private var oldDisplayName: String? = null
|
companion object {
|
||||||
private var oldNote: String? = null
|
const val AVATAR_SIZE = 400
|
||||||
private var oldLocked: Boolean = false
|
const val HEADER_WIDTH = 700
|
||||||
private var isSaving: Boolean = false
|
const val HEADER_HEIGHT = 335
|
||||||
private var currentlyPicking: PickType = PickType.NOTHING
|
|
||||||
private var avatarChanged: Boolean = false
|
private const val AVATAR_PICK_RESULT = 1
|
||||||
private var headerChanged: Boolean = false
|
private const val HEADER_PICK_RESULT = 2
|
||||||
|
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||||
|
private const val MAX_ACCOUNT_FIELDS = 4
|
||||||
|
}
|
||||||
|
|
||||||
@Inject
|
@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 {
|
private enum class PickType {
|
||||||
NOTHING,
|
NOTHING,
|
||||||
@ -94,93 +81,127 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_edit_profile)
|
setContentView(R.layout.activity_edit_profile)
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(this, viewModelFactory)[EditProfileViewModel::class.java]
|
||||||
|
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
setTitle(R.string.title_edit_profile)
|
setTitle(R.string.title_edit_profile)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) }
|
avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) }
|
||||||
headerButton.setOnClickListener { onMediaPick(PickType.HEADER) }
|
headerButton.setOnClickListener { onMediaPick(PickType.HEADER) }
|
||||||
|
|
||||||
avatarPreview.setOnClickListener {
|
fieldList.layoutManager = LinearLayoutManager(this)
|
||||||
avatarPreview.setImageBitmap(null)
|
fieldList.adapter = accountFieldEditAdapter
|
||||||
avatarPreview.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
headerPreview.setOnClickListener {
|
|
||||||
headerPreview.setImageBitmap(null)
|
|
||||||
headerPreview.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
mastodonApi.accountVerifyCredentials().enqueue(object : Callback<Account> {
|
val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).sizeDp(12).color(Color.WHITE)
|
||||||
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
|
|
||||||
|
|
||||||
displayNameEditText.setText(oldDisplayName)
|
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(addFieldButton, plusDrawable, null, null, null)
|
||||||
noteEditText.setText(oldNote)
|
|
||||||
lockedCheckBox.isChecked = oldLocked
|
|
||||||
|
|
||||||
if (!avatarChanged) {
|
addFieldButton.setOnClickListener {
|
||||||
Picasso.with(avatarPreview.context)
|
accountFieldEditAdapter.addField()
|
||||||
.load(me.avatar)
|
if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
|
||||||
.placeholder(R.drawable.avatar_default)
|
it.isEnabled = false
|
||||||
.into(avatarPreview)
|
|
||||||
}
|
|
||||||
if (!headerChanged) {
|
|
||||||
Picasso.with(headerPreview.context)
|
|
||||||
.load(me.header)
|
|
||||||
.into(headerPreview)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
scrollView.post{
|
||||||
onAccountVerifyCredentialsFailed()
|
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) {
|
override fun onStop() {
|
||||||
outState.run {
|
super.onStop()
|
||||||
putString(KEY_OLD_DISPLAY_NAME, oldDisplayName)
|
if(!isFinishing) {
|
||||||
putString(KEY_OLD_NOTE, oldNote)
|
viewModel.updateProfile(displayNameEditText.text.toString(),
|
||||||
putBoolean(KEY_OLD_LOCKED, oldLocked)
|
noteEditText.text.toString(),
|
||||||
putBoolean(KEY_IS_SAVING, isSaving)
|
lockedCheckBox.isChecked,
|
||||||
putSerializable(KEY_CURRENTLY_PICKING, currentlyPicking)
|
accountFieldEditAdapter.getFieldData())
|
||||||
putBoolean(KEY_AVATAR_CHANGED, avatarChanged)
|
|
||||||
putBoolean(KEY_HEADER_CHANGED, headerChanged)
|
|
||||||
}
|
}
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAccountVerifyCredentialsFailed() {
|
private fun observeImage(liveData: LiveData<Resource<Bitmap>>, imageView: ImageView, progressBar: View) {
|
||||||
Log.e(TAG, "The account failed to load.")
|
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) {
|
private fun onMediaPick(pickType: PickType) {
|
||||||
@ -245,77 +266,20 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun save() {
|
private fun save() {
|
||||||
if (isSaving || currentlyPicking != PickType.NOTHING) {
|
if (currentlyPicking != PickType.NOTHING) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isSaving = true
|
viewModel.save(displayNameEditText.text.toString(),
|
||||||
saveProgressBar.visibility = View.VISIBLE
|
noteEditText.text.toString(),
|
||||||
|
lockedCheckBox.isChecked,
|
||||||
val newDisplayName = displayNameEditText.text.toString()
|
accountFieldEditAdapter.getFieldData(),
|
||||||
val displayName = if (oldDisplayName == newDisplayName) {
|
this)
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSaveFailure() {
|
private fun onSaveFailure(msg: String?) {
|
||||||
isSaving = false
|
val errorMsg = msg ?: getString(R.string.error_media_upload_sending)
|
||||||
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(avatarButton, errorMsg, Snackbar.LENGTH_LONG).show()
|
||||||
saveProgressBar.visibility = View.GONE
|
saveProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,6 +314,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||||
CropImage.activity(data.data)
|
CropImage.activity(data.data)
|
||||||
.setInitialCropWindowPaddingRatio(0f)
|
.setInitialCropWindowPaddingRatio(0f)
|
||||||
|
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||||
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
||||||
.start(this)
|
.start(this)
|
||||||
} else {
|
} else {
|
||||||
@ -360,6 +325,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||||
CropImage.activity(data.data)
|
CropImage.activity(data.data)
|
||||||
.setInitialCropWindowPaddingRatio(0f)
|
.setInitialCropWindowPaddingRatio(0f)
|
||||||
|
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||||
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
||||||
.start(this)
|
.start(this)
|
||||||
} else {
|
} else {
|
||||||
@ -379,50 +345,21 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||||||
|
|
||||||
private fun beginResize(uri: Uri) {
|
private fun beginResize(uri: Uri) {
|
||||||
beginMediaPicking()
|
beginMediaPicking()
|
||||||
val width: Int
|
|
||||||
val height: Int
|
|
||||||
val cacheFile: File
|
|
||||||
when (currentlyPicking) {
|
when (currentlyPicking) {
|
||||||
EditProfileActivity.PickType.AVATAR -> {
|
EditProfileActivity.PickType.AVATAR -> {
|
||||||
width = AVATAR_SIZE
|
viewModel.newAvatar(uri, this)
|
||||||
height = AVATAR_SIZE
|
|
||||||
cacheFile = getCacheFileForName(AVATAR_FILE_NAME)
|
|
||||||
}
|
}
|
||||||
EditProfileActivity.PickType.HEADER -> {
|
EditProfileActivity.PickType.HEADER -> {
|
||||||
width = HEADER_WIDTH
|
viewModel.newHeader(uri, this)
|
||||||
height = HEADER_HEIGHT
|
|
||||||
cacheFile = getCacheFileForName(HEADER_FILE_NAME)
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw AssertionError("PickType not set.")
|
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() {
|
private fun onResizeFailure() {
|
||||||
@ -430,80 +367,4 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||||||
endMediaPicking()
|
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;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
|
import android.arch.lifecycle.Lifecycle;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -36,6 +36,8 @@ import android.view.KeyEvent;
|
|||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
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.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||||
@ -67,10 +69,14 @@ import javax.inject.Inject;
|
|||||||
import dagger.android.AndroidInjector;
|
import dagger.android.AndroidInjector;
|
||||||
import dagger.android.DispatchingAndroidInjector;
|
import dagger.android.DispatchingAndroidInjector;
|
||||||
import dagger.android.support.HasSupportFragmentInjector;
|
import dagger.android.support.HasSupportFragmentInjector;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
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,
|
public final class MainActivity extends BottomSheetActivity implements ActionButtonActivity,
|
||||||
HasSupportFragmentInjector {
|
HasSupportFragmentInjector {
|
||||||
|
|
||||||
@ -90,6 +96,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public DispatchingAndroidInjector<Fragment> fragmentInjector;
|
public DispatchingAndroidInjector<Fragment> fragmentInjector;
|
||||||
|
@Inject
|
||||||
|
public EventHub eventHub;
|
||||||
|
|
||||||
private FloatingActionButton composeButton;
|
private FloatingActionButton composeButton;
|
||||||
private AccountHeader headerResult;
|
private AccountHeader headerResult;
|
||||||
@ -211,6 +219,15 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||||||
disablePushNotifications();
|
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
|
@Override
|
||||||
@ -219,16 +236,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||||||
|
|
||||||
NotificationHelper.clearNotificationsForActiveAccount(this, accountManager);
|
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
|
@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
|
package com.keylesspalace.tusky.appstore
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
|
||||||
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
|
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 BlockEvent(val accountId: String) : Dispatchable
|
||||||
data class MuteEvent(val accountId: String) : Dispatchable
|
data class MuteEvent(val accountId: String) : Dispatchable
|
||||||
data class StatusDeletedEvent(val statusId: 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.ViewModel
|
||||||
import android.arch.lifecycle.ViewModelProvider
|
import android.arch.lifecycle.ViewModelProvider
|
||||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||||
|
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.MapKey
|
import dagger.MapKey
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
@ -16,7 +17,7 @@ import kotlin.reflect.KClass
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ViewModelFactory @Inject constructor(private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
|
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
|
override fun <T : ViewModel> create(modelClass: Class<T>): T = viewModels[modelClass]?.get() as T
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,5 +37,10 @@ abstract class ViewModelModule {
|
|||||||
@ViewModelKey(AccountViewModel::class)
|
@ViewModelKey(AccountViewModel::class)
|
||||||
internal abstract fun accountViewModel(viewModel: AccountViewModel): ViewModel
|
internal abstract fun accountViewModel(viewModel: AccountViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(EditProfileViewModel::class)
|
||||||
|
internal abstract fun editProfileViewModel(viewModel: EditProfileViewModel): ViewModel
|
||||||
|
|
||||||
//Add more ViewModels here
|
//Add more ViewModels here
|
||||||
}
|
}
|
@ -70,15 +70,22 @@ data class Account(
|
|||||||
data class AccountSource(
|
data class AccountSource(
|
||||||
val privacy: Status.Visibility,
|
val privacy: Status.Visibility,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
val note: String
|
val note: String,
|
||||||
|
val fields: List<StringField>?
|
||||||
): Parcelable
|
): Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Field (
|
data class Field (
|
||||||
val name:String,
|
val name: String,
|
||||||
val value: @WriteWith<SpannedParceler>() Spanned
|
val value: @WriteWith<SpannedParceler>() Spanned
|
||||||
): Parcelable
|
): Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class StringField (
|
||||||
|
val name: String,
|
||||||
|
val value: String
|
||||||
|
): Parcelable
|
||||||
|
|
||||||
object SpannedParceler : Parceler<Spanned> {
|
object SpannedParceler : Parceler<Spanned> {
|
||||||
override fun create(parcel: Parcel): Spanned = HtmlUtils.fromHtml(parcel.readString())
|
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="note") RequestBody note,
|
||||||
@Nullable @Part(value="locked") RequestBody locked,
|
@Nullable @Part(value="locked") RequestBody locked,
|
||||||
@Nullable @Part MultipartBody.Part avatar,
|
@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")
|
@GET("api/v1/accounts/search")
|
||||||
Call<List<Account>> searchAccounts(
|
Call<List<Account>> searchAccounts(
|
||||||
@Query("q") String q,
|
@Query("q") String q,
|
||||||
@Query("resolve") Boolean resolve,
|
@Query("resolve") Boolean resolve,
|
||||||
@Query("limit") Integer limit);
|
@Query("limit") Integer limit);
|
||||||
|
|
||||||
@GET("api/v1/accounts/{id}")
|
@GET("api/v1/accounts/{id}")
|
||||||
Call<Account> account(@Path("id") String accountId);
|
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 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.MutableLiveData
|
||||||
import android.arch.lifecycle.ViewModel
|
import android.arch.lifecycle.ViewModel
|
||||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
import com.keylesspalace.tusky.appstore.*
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
|
||||||
import com.keylesspalace.tusky.appstore.MuteEvent
|
|
||||||
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
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.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
@ -27,6 +25,12 @@ class AccountViewModel @Inject constructor(
|
|||||||
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
||||||
|
|
||||||
private val callList: MutableList<Call<*>> = mutableListOf()
|
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) {
|
fun obtainAccount(accountId: String, reload: Boolean = false) {
|
||||||
@ -182,6 +186,7 @@ class AccountViewModel @Inject constructor(
|
|||||||
callList.forEach {
|
callList.forEach {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
}
|
}
|
||||||
|
disposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class RelationShipAction {
|
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" />
|
<include layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
<ScrollView
|
<android.support.v4.widget.NestedScrollView
|
||||||
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
@ -47,98 +48,130 @@
|
|||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="wrap_content"
|
<LinearLayout
|
||||||
|
android:layout_width="@dimen/timeline_width"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_marginTop="-40dp">
|
android:layout_marginTop="-40dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
<com.keylesspalace.tusky.view.RoundedImageView
|
<RelativeLayout
|
||||||
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"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_marginStart="16dp">
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</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
|
<ImageButton
|
||||||
android:id="@+id/layout_edit_profile_display_name"
|
android:id="@+id/avatarButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="80dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="80dp"
|
||||||
android:layout_marginTop="30dp">
|
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
|
<ProgressBar
|
||||||
android:id="@+id/displayNameEditText"
|
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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:hint="@string/hint_display_name"
|
android:layout_marginTop="30dp"
|
||||||
android:importantForAutofill="no"
|
android:paddingStart="8dp"
|
||||||
android:maxLength="30" />
|
android:text="@string/lock_account_label"
|
||||||
|
android:textSize="?attr/status_text_medium" />
|
||||||
|
|
||||||
</android.support.design.widget.TextInputLayout>
|
<TextView
|
||||||
|
|
||||||
<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"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
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_marginBottom="16dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:hint="@string/hint_note"
|
android:drawablePadding="6dp"
|
||||||
android:importantForAutofill="no"
|
android:text="@string/profile_metadata_add"
|
||||||
android:maxLength="160" />
|
android:textColor="#fff" />
|
||||||
|
|
||||||
</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" />
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</android.support.v4.widget.NestedScrollView>
|
||||||
|
|
||||||
<include layout="@layout/toolbar_shadow_shim" />
|
<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="colorPrimary">@color/color_primary_dark</item>
|
||||||
<item name="colorPrimaryDark">@color/color_primary_dark_dark</item>
|
<item name="colorPrimaryDark">@color/color_primary_dark_dark</item>
|
||||||
<item name="colorAccent">@color/color_accent_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:colorBackground">@color/color_primary_dark_dark</item>
|
||||||
<item name="android:windowBackground">@color/window_background_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_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_apache_2">Licensed under the Apache License (copy below)</string>
|
||||||
<string name="license_cc_by_4">CC-BY 4.0</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>
|
</resources>
|
||||||
|
@ -55,7 +55,8 @@
|
|||||||
<item name="colorPrimary">@color/color_primary_light</item>
|
<item name="colorPrimary">@color/color_primary_light</item>
|
||||||
<item name="colorPrimaryDark">@color/color_primary_dark_light</item>
|
<item name="colorPrimaryDark">@color/color_primary_dark_light</item>
|
||||||
<item name="colorAccent">@color/color_accent_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:colorBackground">@color/color_background_light</item>
|
||||||
<item name="android:windowBackground">@color/window_background_light</item>
|
<item name="android:windowBackground">@color/window_background_light</item>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user