Merge branch 'edit_ux_improvements' into 'master'

Improve UX in post creation and edit activities

Closes #267

See merge request pixeldroid/PixelDroid!285
This commit is contained in:
Matthieu 2021-01-14 11:27:24 +01:00
commit 01879cfd28
70 changed files with 3156 additions and 854 deletions

View File

@ -4,14 +4,14 @@ plugins {
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
android {
compileSdkVersion 30
buildToolsVersion '30.0.2'
buildToolsVersion '30.0.3'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@ -56,6 +56,7 @@ android {
}
buildFeatures {
viewBinding = true
dataBinding = true
}
apply plugin: 'kotlin-kapt'
@ -83,6 +84,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "androidx.annotation:annotation:1.1.0"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
@ -157,6 +159,7 @@ dependencies {
implementation 'com.github.ligi.tracedroid:lib:3.0'
implementation 'com.github.ligi.tracedroid:supportemail:3.0'
implementation 'me.relex:circleindicator:2.1.4'
/**
* Not in release, so not mentioned in licenses list

View File

@ -737,3 +737,31 @@
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: androidx.databinding:databinding-adapters:+
name: databinding-adapters
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.databinding:databinding-runtime:+
name: databinding-runtime
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.databinding:databinding-common:+
name: databinding-common
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/studio
- artifact: androidx.lifecycle:lifecycle-common-java8:+
name: lifecycle-common-java8
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: me.relex:circleindicator:+
name: circleindicator
copyrightHolder: relex and contributors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/ongakuer/CircleIndicator

View File

@ -175,7 +175,7 @@ class HomeFeedTest {
//Open the comment section
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(0, clickChildViewWithId(R.id.ViewComments)))
(0, clickChildViewWithId(R.id.viewComments)))
Thread.sleep(1000)
onView(first(withId(R.id.commentContainer)))
.check(matches(hasDescendant(withId(R.id.comment))))
@ -186,7 +186,7 @@ class HomeFeedTest {
//Open the comment section
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<StatusViewHolder>
(2, clickChildViewWithId(R.id.ViewComments)))
(2, clickChildViewWithId(R.id.viewComments)))
Thread.sleep(1000)
onView(withId(R.id.list)).check(matches(isDisplayed()))
}

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,8 @@ import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.databinding.ActivityLoginBinding
import com.h.pixeldroid.databinding.ActivityPostCreationBinding
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.*
@ -17,7 +19,6 @@ import com.h.pixeldroid.utils.db.storeInstance
import com.h.pixeldroid.utils.hasInternet
import com.h.pixeldroid.utils.normalizeDomain
import com.h.pixeldroid.utils.openUrl
import kotlinx.android.synthetic.main.activity_login.*
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@ -56,9 +57,12 @@ class LoginActivity : BaseActivity() {
private lateinit var pixelfedAPI: PixelfedAPI
private var inputVisibility: Int = View.GONE
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
loadingAnimation(true)
appName = getString(R.string.app_name)
@ -66,14 +70,14 @@ class LoginActivity : BaseActivity() {
preferences = getSharedPreferences("$PACKAGE_ID.pref", Context.MODE_PRIVATE)
if (hasInternet(applicationContext)) {
connect_instance_button.setOnClickListener {
registerAppToServer(normalizeDomain(editText.text.toString()))
binding.connectInstanceButton.setOnClickListener {
registerAppToServer(normalizeDomain(binding.editText.text.toString()))
}
whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
binding.whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
inputVisibility = View.VISIBLE
} else {
login_activity_connection_required.visibility = View.VISIBLE
login_activity_connection_required_button.setOnClickListener {
binding.loginActivityConnectionRequired.visibility = View.VISIBLE
binding.loginActivityConnectionRequiredButton.setOnClickListener {
finish()
startActivity(intent)
}
@ -267,7 +271,7 @@ class LoginActivity : BaseActivity() {
private fun failedRegistration(message: String = getString(R.string.registration_failed)) {
loadingAnimation(false)
editText.error = message
binding.editText.error = message
wipeSharedSettings()
}
@ -278,12 +282,12 @@ class LoginActivity : BaseActivity() {
private fun loadingAnimation(on: Boolean){
if(on) {
login_activity_instance_input_layout.visibility = View.GONE
progressLayout.visibility = View.VISIBLE
binding.loginActivityInstanceInputLayout.visibility = View.GONE
binding.progressLayout.visibility = View.VISIBLE
}
else {
login_activity_instance_input_layout.visibility = inputVisibility
progressLayout.visibility = View.GONE
binding.loginActivityInstanceInputLayout.visibility = inputVisibility
binding.progressLayout.visibility = View.GONE
}
}

View File

@ -14,22 +14,21 @@ import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.room.withTransaction
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.bumptech.glide.Glide
import com.google.android.material.tabs.TabLayoutMediator
import com.h.pixeldroid.utils.db.addUser
import com.h.pixeldroid.databinding.ActivityMainBinding
import com.h.pixeldroid.postCreation.camera.CameraFragment
import com.h.pixeldroid.utils.db.entities.HomeStatusDatabaseEntity
import com.h.pixeldroid.utils.db.entities.PublicFeedStatusDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.posts.feeds.cachedFeeds.notifications.NotificationsFragment
import com.h.pixeldroid.posts.feeds.cachedFeeds.postFeeds.PostFeedFragment
import com.h.pixeldroid.utils.api.objects.Account
import com.h.pixeldroid.profile.ProfileActivity
import com.h.pixeldroid.searchDiscover.SearchDiscoverFragment
import com.h.pixeldroid.settings.SettingsActivity
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.utils.db.addUser
import com.h.pixeldroid.utils.db.entities.HomeStatusDatabaseEntity
import com.h.pixeldroid.utils.db.entities.PublicFeedStatusDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.utils.hasInternet
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.materialdrawer.iconics.iconicsIcon
@ -40,12 +39,8 @@ import com.mikepenz.materialdrawer.model.interfaces.*
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.widget.AccountHeaderView
import kotlinx.android.synthetic.main.activity_main.*
import org.ligi.tracedroid.sending.TraceDroidEmailSender
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
class MainActivity : BaseActivity() {
@ -57,11 +52,14 @@ class MainActivity : BaseActivity() {
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
}
private lateinit var binding: ActivityMainBinding
@ExperimentalPagingApi
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme_NoActionBar)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
TraceDroidEmailSender.sendStackTraces("contact@pixeldroid.org", this)
@ -96,8 +94,8 @@ class MainActivity : BaseActivity() {
}
private fun setupDrawer() {
main_drawer_button.setOnClickListener{
drawer_layout.open()
binding.mainDrawerButton.setOnClickListener{
binding.drawerLayout.open()
}
header = AccountHeaderView(this).apply {
@ -112,7 +110,7 @@ class MainActivity : BaseActivity() {
descriptionRes = R.string.add_account_description
iconicsIcon = GoogleMaterial.Icon.gmd_add
}, 0)
attachToSliderView(drawer)
attachToSliderView(binding.drawer)
dividerBelowHeader = false
closeDrawerOnProfileListClick = true
}
@ -144,7 +142,7 @@ class MainActivity : BaseActivity() {
//with the received one. This happens asynchronously.
getUpdatedAccount()
drawer.itemAdapter.add(
binding.drawer.itemAdapter.add(
primaryDrawerItem {
nameRes = R.string.menu_account
iconicsIcon = GoogleMaterial.Icon.gmd_person
@ -157,7 +155,7 @@ class MainActivity : BaseActivity() {
nameRes = R.string.logout
iconicsIcon = GoogleMaterial.Icon.gmd_close
})
drawer.onDrawerItemClickListener = { v, drawerItem, position ->
binding.drawer.onDrawerItemClickListener = { v, drawerItem, position ->
when (position){
1 -> launchActivity(ProfileActivity())
2 -> launchActivity(SettingsActivity())
@ -271,7 +269,7 @@ class MainActivity : BaseActivity() {
private fun setupTabs(tab_array: List<() -> Fragment>){
view_pager.adapter = object : FragmentStateAdapter(this) {
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): Fragment {
return tab_array[position]()
}
@ -281,7 +279,7 @@ class MainActivity : BaseActivity() {
}
}
TabLayoutMediator(tabs, view_pager) { tab, position ->
TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
tab.icon = ContextCompat.getDrawable(applicationContext,
when(position){
0 -> R.drawable.ic_home_white_24dp
@ -312,8 +310,8 @@ class MainActivity : BaseActivity() {
* Closes the drawer if it is open, when we press the back button
*/
override fun onBackPressed() {
if(drawer_layout.isDrawerOpen(GravityCompat.START)){
drawer_layout.closeDrawer(GravityCompat.START)
if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){
binding.drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}

View File

@ -1,75 +1,86 @@
package com.h.pixeldroid.postCreation
import android.app.Activity
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.Button
import android.widget.ImageButton
import android.widget.Toast
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.MainActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ActivityPostCreationBinding
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.postCreation.camera.CameraActivity
import com.h.pixeldroid.postCreation.carousel.CarouselItem
import com.h.pixeldroid.postCreation.carousel.ImageCarousel
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.utils.api.objects.Attachment
import com.h.pixeldroid.utils.api.objects.Instance
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.postCreation.photoEdit.PhotoEditActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_post_creation.*
import kotlinx.android.synthetic.main.image_album_creation.view.*
import okhttp3.MultipartBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
private const val TAG = "Post Creation Activity"
private const val MORE_PICTURES_REQUEST_CODE = 0xffff
data class PhotoData(
var imageUri: Uri,
var uploadId: String? = null,
var progress: Int? = null
)
class PostCreationActivity : BaseActivity() {
private lateinit var recycler : RecyclerView
private lateinit var adapter : PostCreationAdapter
private lateinit var accessToken: String
private lateinit var pixelfedAPI: PixelfedAPI
private var muListOfIds: MutableList<String> = mutableListOf()
private var progressList: ArrayList<Int> = arrayListOf()
private var positionResult = 0
private var user: UserDatabaseEntity? = null
private var posts: ArrayList<String> = ArrayList()
private val photoData: ArrayList<PhotoData> = ArrayList()
private lateinit var binding: ActivityPostCreationBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post_creation)
binding = ActivityPostCreationBinding.inflate(layoutInflater)
setContentView(binding.root)
// get image URIs
if(intent.clipData != null) {
val count = intent.clipData!!.itemCount
for (i in 0 until count) {
val imageUri: String = intent.clipData!!.getItemAt(i).uri.toString()
posts.add(imageUri)
intent.clipData!!.getItemAt(i).uri.let {
photoData.add(PhotoData(it))
}
}
}
@ -77,9 +88,7 @@ class PostCreationActivity : BaseActivity() {
val instances = db.instanceDao().getAll()
val textField = findViewById<TextInputLayout>(R.id.postTextInputLayout)
textField.counterMaxLength = if (user != null){
binding.postTextInputLayout.counterMaxLength = if (user != null){
val thisInstances =
instances.filter { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(user!!.instance_uri)
@ -92,64 +101,155 @@ class PostCreationActivity : BaseActivity() {
accessToken = user?.accessToken.orEmpty()
pixelfedAPI = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
// check if the pictures are alright
// TODO
//upload the picture and display progress while doing so
muListOfIds = posts.map { "" }.toMutableList()
progressList = posts.map { 0 } as ArrayList<Int>
upload()
adapter = PostCreationAdapter(posts)
recycler = findViewById(R.id.image_grid)
recycler.layoutManager = GridLayoutManager(this, 3)
recycler.adapter = adapter
val carousel: ImageCarousel = binding.carousel
carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) })
carousel.layoutCarouselCallback = {
//TODO transition instead of at once
if(it){
// Became a carousel
binding.toolbar3.visibility = VISIBLE
} else {
// Became a grid
binding.toolbar3.visibility = INVISIBLE
}
}
carousel.addPhotoButtonCallback = {
addPhoto(applicationContext)
}
// get the description and send the post
findViewById<Button>(R.id.post_creation_send_button).setOnClickListener {
if (validateDescription() && muListOfIds.isNotEmpty()) post()
binding.postCreationSendButton.setOnClickListener {
if (validateDescription() && photoData.isNotEmpty()) upload()
}
// Button to retry image upload when it fails
findViewById<Button>(R.id.retry_upload_button).setOnClickListener {
upload_error.visibility = View.GONE
muListOfIds = posts.map { "" }.toMutableList()
progressList = posts.map { 0 } as ArrayList<Int>
binding.retryUploadButton.setOnClickListener {
binding.uploadError.visibility = View.GONE
photoData.forEach {
it.uploadId = null
it.progress = null
}
upload()
}
binding.editPhotoButton.setOnClickListener {
carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
edit(currentPosition)
}
}
binding.addPhotoButton.setOnClickListener {
addPhoto(it.context)
}
binding.savePhotoButton.setOnClickListener {
carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
savePicture(it, currentPosition)
}
}
binding.removePhotoButton.setOnClickListener {
carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
photoData.removeAt(currentPosition)
carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) })
}
}
}
private fun addPhoto(context: Context){
val intent = Intent(context, CameraActivity::class.java)
this@PostCreationActivity.startActivityForResult(intent, MORE_PICTURES_REQUEST_CODE)
}
private fun savePicture(button: View, currentPosition: Int) {
val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis()) + ".png"
val pair = getOutputFile(name)
val outputStream: OutputStream = pair.first
val path: String = pair.second
contentResolver.openInputStream(photoData[currentPosition].imageUri)!!.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
if(path.startsWith("file")) {
MediaScannerConnection.scanFile(
this,
arrayOf(path.toUri().toFile().absolutePath),
null
) { path, uri ->
if (uri == null) {
Log.e(
"NEW IMAGE SCAN FAILED",
"Tried to scan $path, but it failed"
)
}
}
}
Snackbar.make(
button, getString(R.string.save_image_success),
Snackbar.LENGTH_LONG
).show()
}
private fun getOutputFile(name: String): Pair<OutputStream, String> {
val outputStream: OutputStream
val path: String
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver: ContentResolver = contentResolver
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
contentValues.put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES
)
val imageUri: Uri =
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)!!
path = imageUri.toString()
outputStream = resolver.openOutputStream(Objects.requireNonNull(imageUri))!!
} else {
@Suppress("DEPRECATION") val imagesDir =
Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
imagesDir.mkdir()
val file = File(imagesDir, name)
path = Uri.fromFile(file).toString()
outputStream = file.outputStream()
}
return Pair(outputStream, path)
}
private fun validateDescription(): Boolean {
val textField = findViewById<TextInputLayout>(R.id.postTextInputLayout)
val content = textField.editText?.length() ?: 0
if (content > textField.counterMaxLength) {
// error, too many characters
textField.error = getString(R.string.description_max_characters).format(textField.counterMaxLength)
return false
binding.postTextInputLayout.run {
val content = editText?.length() ?: 0
if (content > counterMaxLength) {
// error, too many characters
error = getString(R.string.description_max_characters).format(counterMaxLength)
return false
}
}
return true
}
/**
* Uploads the images that are in the [posts] array.
* Keeps track of them in the [progressList] (for the upload progress), and the [muListOfIds]
* (for the list of ids of the uploads).
* @param newImagesStartingIndex is the index in the [posts] array we want to start uploading at.
* Indices before this are already uploading, or done uploading, from before.
* @param editedImage contains the index of the image that was edited. If set, other images are
* not uploaded again: they should already be uploading, or be done uploading, from before.
* Uploads the images that are in the [photoData] array.
* Keeps track of them in the [PhotoData.progress] (for the upload progress), and the
* [PhotoData.uploadId] (for the list of ids of the uploads).
*/
private fun upload(newImagesStartingIndex: Int = 0, editedImage: Int? = null) {
private fun upload() {
enableButton(false)
uploadProgressBar.visibility = View.VISIBLE
upload_completed_textview.visibility = View.INVISIBLE
binding.uploadProgressBar.visibility = View.VISIBLE
binding.uploadCompletedTextview.visibility = View.INVISIBLE
binding.removePhotoButton.isEnabled = false
binding.editPhotoButton.isEnabled = false
binding.addPhotoButton.isEnabled = false
val range: IntRange = if(editedImage == null){
newImagesStartingIndex until posts.size
} else IntRange(editedImage, editedImage)
for (index in range) {
val imageUri = Uri.parse(posts[index])
for (data: PhotoData in photoData) {
val imageUri = data.imageUri
val imageInputStream = contentResolver.openInputStream(imageUri)!!
val size =
@ -177,9 +277,9 @@ class PostCreationActivity : BaseActivity() {
val sub = imagePart.progressSubject
.subscribeOn(Schedulers.io())
.subscribe { percentage ->
progressList[index] = percentage.toInt()
uploadProgressBar.progress =
progressList.sum() / progressList.size
data.progress = percentage.toInt()
binding.uploadProgressBar.progress =
photoData.sumBy { it.progress ?: 0 } / photoData.size
}
var postSub: Disposable? = null
@ -190,21 +290,21 @@ class PostCreationActivity : BaseActivity() {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ attachment: Attachment ->
progressList[index] = 0
muListOfIds[index] = attachment.id!!
data.progress = 0
data.uploadId = attachment.id!!
},
{ e ->
upload_error.visibility = View.VISIBLE
binding.uploadError.visibility = View.VISIBLE
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
{
progressList[index] = 100
if(progressList.all{it == 100}){
enableButton(true)
uploadProgressBar.visibility = View.GONE
upload_completed_textview.visibility = View.VISIBLE
data.progress = 100
if(photoData.all{it.progress == 100}){
binding.uploadProgressBar.visibility = View.GONE
binding.uploadCompletedTextview.visibility = View.VISIBLE
post()
}
postSub?.dispose()
sub.dispose()
@ -214,14 +314,14 @@ class PostCreationActivity : BaseActivity() {
}
private fun post() {
val description = new_post_description_input_field.text.toString()
val description = binding.newPostDescriptionInputField.text.toString()
enableButton(false)
lifecycleScope.launchWhenCreated {
try {
pixelfedAPI.postStatus(
authorization = "Bearer $accessToken",
statusText = description,
media_ids = muListOfIds.toList()
media_ids = photoData.mapNotNull { it.uploadId }.toList()
)
Toast.makeText(applicationContext,getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
@ -243,22 +343,22 @@ class PostCreationActivity : BaseActivity() {
}
private fun enableButton(enable: Boolean = true){
post_creation_send_button.isEnabled = enable
binding.postCreationSendButton.isEnabled = enable
if(enable){
posting_progress_bar.visibility = View.GONE
post_creation_send_button.visibility = View.VISIBLE
binding.postingProgressBar.visibility = View.GONE
binding.postCreationSendButton.visibility = View.VISIBLE
} else {
posting_progress_bar.visibility = View.VISIBLE
post_creation_send_button.visibility = View.GONE
binding.postingProgressBar.visibility = View.VISIBLE
binding.postCreationSendButton.visibility = View.GONE
}
}
fun onClick(position: Int) {
private fun edit(position: Int) {
positionResult = position
val intent = Intent(this, PhotoEditActivity::class.java)
.putExtra("picture_uri", Uri.parse(posts[position]))
.putExtra("picture_uri", photoData[position].imageUri)
.putExtra("no upload", false)
startActivityForResult(intent, positionResult)
}
@ -267,14 +367,13 @@ class PostCreationActivity : BaseActivity() {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == positionResult) {
if (resultCode == Activity.RESULT_OK && data != null) {
posts[positionResult] = data.getStringExtra("result")!!
adapter.notifyItemChanged(positionResult)
muListOfIds[positionResult] = ""
progressList[positionResult] = 0
upload(editedImage = positionResult)
} else if(resultCode == Activity.RESULT_CANCELED){
Toast.makeText(applicationContext, "Editing canceled", Toast.LENGTH_SHORT).show()
} else {
photoData[positionResult].imageUri = data.getStringExtra("result")!!.toUri()
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) })
photoData[positionResult].progress = null
photoData[positionResult].uploadId = null
} else if(resultCode != Activity.RESULT_CANCELED){
Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
}
} else if (requestCode == MORE_PICTURES_REQUEST_CODE) {
@ -282,71 +381,14 @@ class PostCreationActivity : BaseActivity() {
val count = data.clipData!!.itemCount
for (i in 0 until count) {
val imageUri: String = data.clipData!!.getItemAt(i).uri.toString()
posts.add(imageUri)
progressList.add(0)
muListOfIds.add("")
val imageUri: Uri = data.clipData!!.getItemAt(i).uri
photoData.add(PhotoData(imageUri))
}
adapter.notifyDataSetChanged()
upload(newImagesStartingIndex = posts.size - count)
} else if(resultCode == Activity.RESULT_CANCELED){
Toast.makeText(applicationContext, "Adding images canceled", Toast.LENGTH_SHORT).show()
} else {
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) })
} else if(resultCode != Activity.RESULT_CANCELED){
Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show()
}
}
}
inner class PostCreationAdapter(private val posts: ArrayList<String>): RecyclerView.Adapter<PostCreationAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view =
if(viewType == 0) LayoutInflater.from(parent.context)
.inflate(R.layout.image_album_creation, parent, false)
else LayoutInflater.from(parent.context)
.inflate(R.layout.add_more_album_creation, parent, false)
return ViewHolder(view)
}
override fun getItemViewType(position: Int): Int {
if(position == posts.size) return 1
return 0
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if(position != posts.size) {
holder.bindImage()
} else{
holder.bindPlusButton()
}
}
override fun getItemCount(): Int = posts.size + 1
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindImage() {
val image = Uri.parse(
posts[adapterPosition]
)
// load image
Glide.with(itemView.context)
.load(image)
.centerCrop()
.into(itemView.galleryImage)
// adding click or tap handler for the image layout
itemView.setOnClickListener {
this@PostCreationActivity.onClick(adapterPosition)
}
}
fun bindPlusButton() {
itemView.setOnClickListener {
val intent = Intent(itemView.context, CameraActivity::class.java)
this@PostCreationActivity.startActivityForResult(intent, MORE_PICTURES_REQUEST_CODE)
}
}
}
}
}

View File

@ -169,7 +169,7 @@ class CameraFragment : Fragment() {
}
/** Declare and bind preview, capture and analysis use cases */
private fun bindCameraUseCases() {
private fun bindCameraUseCases(forceRebind: Boolean = false) {
// Get screen metrics used to setup camera for full screen resolution
val metrics = DisplayMetrics().also { viewFinder.display?.getRealMetrics(it) }
@ -188,7 +188,7 @@ class CameraFragment : Fragment() {
// CameraProvider
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
if (camera == null || preview == null || imageCapture == null || !cameraProvider.isBound(preview!!) || !cameraProvider.isBound(imageCapture!!)) {
if (forceRebind || camera == null || preview == null || imageCapture == null || !cameraProvider.isBound(preview!!) || !cameraProvider.isBound(imageCapture!!)) {
// Preview
@ -324,7 +324,7 @@ class CameraFragment : Fragment() {
REQUEST_CODE_PERMISSIONS
)
} else {
bindCameraUseCases()
bindCameraUseCases(forceRebind = true)
}
}
}

View File

@ -0,0 +1,110 @@
package com.h.pixeldroid.postCreation.carousel
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.h.pixeldroid.R
class CarouselAdapter(
@LayoutRes private val itemLayout: Int,
@IdRes private val imageViewId: Int,
var listener: OnItemClickListener? = null,
private val imageScaleType: ImageView.ScaleType,
private val imagePlaceholder: Drawable?,
private val carousel: Boolean
) : RecyclerView.Adapter<CarouselAdapter.MyViewHolder>() {
private val dataList: MutableList<CarouselItem> = mutableListOf()
class MyViewHolder(itemView: View, imageViewId: Int) : RecyclerView.ViewHolder(itemView) {
var img: ImageView = itemView.findViewById(imageViewId)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return if(!carousel){
if(viewType == 0) {
val view =
LayoutInflater.from(parent.context)
.inflate(R.layout.image_album_creation, parent, false)
MyViewHolder(view, R.id.galleryImage)
} else {
val view =
LayoutInflater.from(parent.context)
.inflate(R.layout.add_more_album_creation, parent, false)
MyViewHolder(view, R.id.addPhotoSquare)
}
} else {
val view = LayoutInflater.from(parent.context)
.inflate(itemLayout, parent, false)
MyViewHolder(view, imageViewId)
}
}
override fun getItemCount(): Int {
return if(carousel) dataList.size
else dataList.size + 1
}
override fun getItemViewType(position: Int): Int {
if(position == dataList.size) return 1
return 0
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
if(carousel) {
holder.img.scaleType = imageScaleType
}
dataList.elementAtOrNull(position)?.let {
Glide.with(holder.itemView.context)
.load(it.imageUrl)
.placeholder(imagePlaceholder)
.into(holder.img)
}
// Init listeners
listener?.apply {
holder.itemView.setOnClickListener {
this.onClick(position)
}
holder.itemView.setOnLongClickListener {
this.onLongClick(position)
true
}
}
}
fun getItem(position: Int): CarouselItem? {
return if (position < dataList.size) {
dataList[position]
} else {
null
}
}
fun addAll(dataList: List<CarouselItem>) {
this.dataList.clear()
this.dataList.addAll(dataList)
notifyDataSetChanged()
}
fun add(item: CarouselItem) {
this.dataList.add(item)
notifyItemInserted(dataList.size - 1)
}
}

View File

@ -0,0 +1,8 @@
package com.h.pixeldroid.postCreation.carousel
data class CarouselItem constructor(
val imageUrl: String? = null,
val caption: String? = null
) {
constructor(imageUrl: String? = null) : this(imageUrl, null)
}

View File

@ -0,0 +1,17 @@
package com.h.pixeldroid.postCreation.carousel
import android.content.Context
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class CarouselLinearLayoutManager(
context: Context,
orientation: Int,
reverseLayout: Boolean
) : LinearLayoutManager(context, orientation, reverseLayout) {
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
super.onLayoutChildren(recycler, state)
scrollHorizontallyBy(0, recycler, state)
}
}

View File

@ -0,0 +1,615 @@
package com.h.pixeldroid.postCreation.carousel
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.Dimension
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.*
import com.h.pixeldroid.R
import me.relex.circleindicator.CircleIndicator2
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable
class ImageCarousel(
@NotNull context: Context,
@Nullable private var attributeSet: AttributeSet?
) : ConstraintLayout(context, attributeSet), OnItemClickListener {
private var adapter: CarouselAdapter? = null
private val scaleTypeArray = arrayOf(
ImageView.ScaleType.MATRIX,
ImageView.ScaleType.FIT_XY,
ImageView.ScaleType.FIT_START,
ImageView.ScaleType.FIT_CENTER,
ImageView.ScaleType.FIT_END,
ImageView.ScaleType.CENTER,
ImageView.ScaleType.CENTER_CROP,
ImageView.ScaleType.CENTER_INSIDE
)
private lateinit var carouselView: View
private lateinit var recyclerView: RecyclerView
private lateinit var tvCaption: TextView
private lateinit var previousButtonContainer: FrameLayout
private lateinit var nextButtonContainer: FrameLayout
private var snapHelper: SnapHelper = PagerSnapHelper()
var indicator: CircleIndicator2? = null
set(newIndicator) {
indicator?.apply {
// if we remove it form the view, then the caption textView constraint won't work.
this.visibility = View.GONE
isBuiltInIndicator = false
}
field = newIndicator
initIndicator()
}
private var btnPrevious: View? = null
private var btnNext: View? = null
private var btnGrid: View? = null
private var btnCarousel: View? = null
private var isBuiltInIndicator = false
private var data: List<CarouselItem>? = null
var onItemClickListener: OnItemClickListener? = this
set(value) {
field = value
adapter?.listener = onItemClickListener
}
var onScrollListener: CarouselOnScrollListener? = null
set(value) {
field = value
initOnScrollStateChange()
}
/**
* Get or set current item position
*/
var currentPosition = -1
get() {
return snapHelper.getSnapPosition(recyclerView.layoutManager)
}
set(value) {
val position = when {
value >= data?.size ?: 0 -> {
-1
}
value < 0 -> {
-1
}
else -> {
value
}
}
field = position
if (position != -1) {
recyclerView.smoothScrollToPosition(position)
}
}
/**
* ****************************************************************
* Attributes
* ****************************************************************
*/
var showCaption = false
set(value) {
field = value
tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE
}
@Dimension(unit = Dimension.PX)
var captionTextSize: Int = 0
set(value) {
field = value
tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat())
}
var showIndicator = false
set(value) {
field = value
when {
indicator == null -> initIndicator()
value -> indicator?.visibility = View.VISIBLE
else -> indicator?.visibility = View.INVISIBLE
}
}
var showNavigationButtons = false
set(value) {
field = value
previousButtonContainer.visibility =
if (showNavigationButtons) View.VISIBLE else View.GONE
nextButtonContainer.visibility =
if (showNavigationButtons) View.VISIBLE else View.GONE
}
var imageScaleType: ImageView.ScaleType = ImageView.ScaleType.CENTER_INSIDE
set(value) {
field = value
initAdapter()
}
var carouselBackground: Drawable? = null
set(value) {
field = value
recyclerView.background = carouselBackground
}
var imagePlaceholder: Drawable? = null
set(value) {
field = value
initAdapter()
}
@LayoutRes
var itemLayout: Int = R.layout.item_carousel
set(value) {
field = value
initAdapter()
}
@IdRes
var imageViewId: Int = R.id.img
set(value) {
field = value
initAdapter()
}
@LayoutRes
var previousButtonLayout: Int = R.layout.previous_button_layout
set(value) {
field = value
btnPrevious = null
previousButtonContainer.removeAllViews()
LayoutInflater.from(context).apply {
inflate(previousButtonLayout, previousButtonContainer, true)
}
}
@IdRes
var previousButtonId: Int = R.id.btn_next
set(value) {
field = value
btnPrevious = carouselView.findViewById(previousButtonId)
btnPrevious?.setOnClickListener {
previous()
}
}
@Dimension(unit = Dimension.PX)
var previousButtonMargin: Int = 0
set(value) {
field = value
val previousButtonParams = previousButtonContainer.layoutParams as LayoutParams
previousButtonParams.setMargins(
previousButtonMargin,
0,
0,
0
)
previousButtonContainer.layoutParams = previousButtonParams
}
@LayoutRes
var nextButtonLayout: Int = R.layout.next_button_layout
set(value) {
field = value
btnNext = null
nextButtonContainer.removeAllViews()
LayoutInflater.from(context).apply {
inflate(nextButtonLayout, nextButtonContainer, true)
}
}
@IdRes
var nextButtonId: Int = R.id.btn_previous
set(value) {
field = value
btnNext = carouselView.findViewById(nextButtonId)
btnNext?.setOnClickListener {
next()
}
}
@Dimension(unit = Dimension.PX)
var nextButtonMargin: Int = 0
set(value) {
field = value
val nextButtonParams = nextButtonContainer.layoutParams as LayoutParams
nextButtonParams.setMargins(
0,
0,
nextButtonMargin,
0
)
nextButtonContainer.layoutParams = nextButtonParams
}
var showLayoutSwitchButton: Boolean = true
set(value) {
field = value
btnGrid = findViewById<ImageButton>(R.id.switchToGridButton)
btnCarousel = findViewById<ImageButton>(R.id.switchToCarouselButton)
btnGrid?.setOnClickListener {
layoutCarousel = false
}
btnCarousel?.setOnClickListener {
layoutCarousel = true
}
if(value){
if(layoutCarousel){
btnGrid?.visibility = VISIBLE
btnCarousel?.visibility = GONE
} else {
btnGrid?.visibility = GONE
btnCarousel?.visibility = VISIBLE
}
} else {
btnGrid?.visibility = GONE
btnCarousel?.visibility = GONE
}
}
var layoutCarouselCallback: ((Boolean) -> Unit)? = null
var layoutCarousel: Boolean = true
set(value) {
field = value
if(value){
recyclerView.layoutManager = CarouselLinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
btnNext?.visibility = VISIBLE
btnPrevious?.visibility = VISIBLE
} else {
recyclerView.layoutManager = GridLayoutManager(context, 3)
btnNext?.visibility = GONE
btnPrevious?.visibility = GONE
}
showIndicator = value
layoutCarouselCallback?.let { it(value) }
//update layout switch button to make it take into account the change
showLayoutSwitchButton = showLayoutSwitchButton
initAdapter()
}
var addPhotoButtonCallback: (() -> Unit)? = null
init {
initViews()
initAttributes()
initAdapter()
initListeners()
}
private fun initViews() {
carouselView = LayoutInflater.from(context).inflate(R.layout.image_carousel, this)
recyclerView = carouselView.findViewById(R.id.recyclerView)
tvCaption = carouselView.findViewById(R.id.tv_caption)
previousButtonContainer = carouselView.findViewById(R.id.previous_button_container)
nextButtonContainer = carouselView.findViewById(R.id.next_button_container)
recyclerView.setHasFixedSize(true)
// For marquee effect
tvCaption.isSelected = true
}
private fun initAttributes() {
context.theme.obtainStyledAttributes(
attributeSet,
R.styleable.ImageCarousel,
0,
0
).apply {
try {
showCaption = getBoolean(
R.styleable.ImageCarousel_showCaption,
true
)
captionTextSize = getDimension(
R.styleable.ImageCarousel_captionTextSize,
14.spToPx(context).toFloat()
).toInt()
showIndicator = getBoolean(
R.styleable.ImageCarousel_showIndicator,
true
)
imageScaleType = scaleTypeArray[
getInteger(
R.styleable.ImageCarousel_imageScaleType,
ImageView.ScaleType.CENTER_INSIDE.ordinal
)
]
carouselBackground = ColorDrawable(Color.parseColor("#333333"))
imagePlaceholder = getDrawable(
R.styleable.ImageCarousel_imagePlaceholder
) ?: ContextCompat.getDrawable(context, R.drawable.ic_picture_fallback)
itemLayout = getResourceId(
R.styleable.ImageCarousel_itemLayout,
R.layout.item_carousel
)
imageViewId = getResourceId(
R.styleable.ImageCarousel_imageViewId,
R.id.img
)
previousButtonLayout = R.layout.previous_button_layout
previousButtonId = R.id.btn_previous
previousButtonMargin = 4.dpToPx(context)
nextButtonLayout = R.layout.next_button_layout
nextButtonId = R.id.btn_next
nextButtonMargin = 4.dpToPx(context)
showNavigationButtons = getBoolean(
R.styleable.ImageCarousel_showNavigationButtons,
true
)
layoutCarousel = getBoolean(
R.styleable.ImageCarousel_layoutCarousel,
true
)
showLayoutSwitchButton = getBoolean(
R.styleable.ImageCarousel_showLayoutSwitchButton,
true
)
} finally {
recycle()
}
}
}
private fun initAdapter() {
adapter = CarouselAdapter(
itemLayout = itemLayout,
imageViewId = imageViewId,
listener = onItemClickListener,
imageScaleType = imageScaleType,
imagePlaceholder = imagePlaceholder,
carousel = layoutCarousel
)
recyclerView.adapter = adapter
data?.apply {
adapter?.addAll(this)
}
indicator?.apply {
try {
adapter?.registerAdapterDataObserver(this.adapterDataObserver)
} catch (e: IllegalStateException) {
e.printStackTrace()
}
}
}
private fun initListeners() {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (showCaption) {
val position = snapHelper.getSnapPosition(recyclerView.layoutManager)
if (position >= 0) {
val dataItem = adapter?.getItem(position)
dataItem?.apply {
tvCaption.text = this.caption
}
}
}
onScrollListener?.onScrolled(recyclerView, dx, dy)
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
onScrollListener?.apply {
val position = snapHelper.getSnapPosition(recyclerView.layoutManager)
val carouselItem = data?.get(position)
onScrollStateChanged(
recyclerView,
newState,
position,
carouselItem
)
}
}
})
}
private fun initIndicator() {
// If no custom indicator added, then default indicator will be shown.
if (indicator == null) {
indicator = carouselView.findViewById(R.id.indicator)
isBuiltInIndicator = true
}
snapHelper.apply {
try {
attachToRecyclerView(recyclerView)
} catch (e: IllegalStateException) {
e.printStackTrace()
}
}
indicator?.apply {
if (isBuiltInIndicator) {
// Indicator visibility
this.visibility = if (showIndicator) View.VISIBLE else View.INVISIBLE
}
// Attach to recyclerview
attachToRecyclerView(recyclerView, snapHelper)
// Observe the adapter
adapter?.let { carouselAdapter ->
try {
carouselAdapter.registerAdapterDataObserver(this.adapterDataObserver)
} catch (e: IllegalStateException) {
e.printStackTrace()
}
}
}
}
private fun initOnScrollStateChange() {
data?.apply {
if (isNotEmpty()) {
onScrollListener?.onScrollStateChanged(
recyclerView,
RecyclerView.SCROLL_STATE_IDLE,
0,
this[0]
)
}
}
}
/**
* Add data to the carousel.
*/
fun addData(data: List<CarouselItem>) {
adapter?.apply {
addAll(data)
this@ImageCarousel.data = data
initOnScrollStateChange()
}
}
/**
* Goto previous item.
*/
fun previous() {
currentPosition--
}
/**
* Goto next item.
*/
fun next() {
currentPosition++
}
override fun onClick(position: Int) {
if(position == (data?.size ?: 0) ){
addPhotoButtonCallback?.invoke()
} else {
if (!layoutCarousel) layoutCarousel = true
currentPosition = position
}
}
override fun onLongClick(position: Int) {
//if(!layoutCarousel && position != (data?.size ?: 0) ) {
//TODO Highlight selected, show toolbar?
// Enable "long click mode?"
//}
}
}
interface OnItemClickListener {
fun onClick(position: Int)
fun onLongClick(position: Int)
}
interface CarouselOnScrollListener {
fun onScrollStateChanged(
recyclerView: RecyclerView,
newState: Int,
position: Int,
carouselItem: CarouselItem?
) {}
fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {}
}

View File

@ -0,0 +1,52 @@
package com.h.pixeldroid.postCreation.carousel
import android.content.Context
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SnapHelper
/**
* This method converts device specific pixels to density independent pixels.
*/
fun Int.pxToDp(context: Context): Int {
return (this / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
}
/**
* This method converts dp unit to equivalent pixels, depending on device density.
*/
fun Int.dpToPx(context: Context): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
context.resources.displayMetrics
).toInt()
}
/**
* This method converts sp unit to equivalent pixels, depending on device density.
*/
fun Int.spToPx(context: Context): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
this.toFloat(),
context.resources.displayMetrics
).toInt()
}
/**
* Get current snap item position of a recyclerView.
*
* @param layoutManager Target recyclerView
* @return Position of the item or RecyclerView.NO_POSITION (-1)
*/
fun SnapHelper.getSnapPosition(layoutManager: RecyclerView.LayoutManager?): Int {
if (layoutManager == null) {
return RecyclerView.NO_POSITION
}
val snapView: View = this.findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
return layoutManager.getPosition(snapView)
}

View File

@ -43,46 +43,50 @@ class EditImageFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
seekbarSaturation.max = SATURATION_MAX
seekbarSaturation.progress = SATURATION_START
seekbarBrightness.setOnSeekBarChangeListener(this)
seekbarContrast.setOnSeekBarChangeListener(this)
seekbarSaturation.setOnSeekBarChangeListener(this)
setOnSeekBarChangeListeners(this)
return view
}
private fun setOnSeekBarChangeListeners(listener: EditImageFragment?){
seekbarBrightness.setOnSeekBarChangeListener(listener)
seekbarContrast.setOnSeekBarChangeListener(listener)
seekbarSaturation.setOnSeekBarChangeListener(listener)
}
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
var prog = progress
if(listener != null) {
listener?.let {
when(seekBar!!.id) {
R.id.seekbar_brightness -> listener!!.onBrightnessChange(progress - 100)
R.id.seekbar_brightness -> it.onBrightnessChange(progress - 100)
R.id.seekbar_saturation -> {
prog += 10
val tempProgress = .10f * prog
listener!!.onSaturationChange(tempProgress)
it.onSaturationChange(.10f * prog)
}
R.id.seekbar_contrast -> {
val tempProgress = .10f * prog
listener!!.onContrastChange(tempProgress)
it.onContrastChange(.10f * prog)
}
}
}
}
fun resetControl() {
// Make sure to ignore seekbar change events, since we don't want to have the reset cause
// filter applications due to the onProgressChanged calls
setOnSeekBarChangeListeners(null)
seekbarBrightness.progress = BRIGHTNESS_START
seekbarContrast.progress = CONTRAST_START
seekbarSaturation.progress = SATURATION_START
setOnSeekBarChangeListeners(this)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
if(listener != null)
listener!!.onEditStarted()
listener?.onEditStarted()
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
if(listener != null)
listener!!.onEditCompleted()
listener?.onEditCompleted()
}
fun setListener(listener: PhotoEditActivity) {

View File

@ -32,7 +32,6 @@ class FilterListFragment : Fragment() {
val view = inflater.inflate(R.layout.fragment_filter_list, container, false)
tbItemList = ArrayList()
adapter = ThumbnailAdapter(requireActivity(), tbItemList, this)
recyclerView = view.findViewById(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)
@ -40,6 +39,8 @@ class FilterListFragment : Fragment() {
val space = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics).toInt()
recyclerView.addItemDecoration(SpaceItemDecoration(space))
adapter = ThumbnailAdapter(requireActivity(), tbItemList, this)
recyclerView.adapter = adapter
return view
@ -99,10 +100,13 @@ class FilterListFragment : Fragment() {
}
}
fun resetSelectedFilter(){
adapter.resetSelected()
displayImage(null)
}
fun onFilterSelected(filter: Filter) {
if(listener != null ){
listener!!.onFilterSelected(filter)
}
listener?.onFilterSelected(filter)
}
fun setListener(listFragmentListener: PhotoEditActivity) {

View File

@ -2,20 +2,14 @@ package com.h.pixeldroid.postCreation.photoEdit
import android.app.Activity
import android.app.AlertDialog
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.drawable.BitmapDrawable
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View.GONE
@ -23,26 +17,23 @@ import android.view.View.VISIBLE
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.bumptech.glide.Glide
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ActivityPhotoEditBinding
import com.h.pixeldroid.databinding.ActivityPostCreationBinding
import com.h.pixeldroid.postCreation.PostCreationActivity
import com.h.pixeldroid.utils.BaseActivity
import com.yalantis.ucrop.UCrop
import com.zomato.photofilters.imageprocessors.Filter
import com.zomato.photofilters.imageprocessors.subfilters.BrightnessSubFilter
import com.zomato.photofilters.imageprocessors.subfilters.ContrastSubFilter
import com.zomato.photofilters.imageprocessors.subfilters.SaturationSubfilter
import kotlinx.android.synthetic.main.activity_photo_edit.*
import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors.newSingleThreadExecutor
import java.util.concurrent.Future
@ -50,10 +41,11 @@ import java.util.concurrent.Future
// This is an arbitrary number we are using to keep track of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts.
private const val REQUEST_CODE_PERMISSIONS_SAVE_PHOTO = 8
private const val REQUEST_CODE_PERMISSIONS_SEND_PHOTO = 7
private val REQUIRED_PERMISSIONS = arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
private val REQUIRED_PERMISSIONS = arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
class PhotoEditActivity : BaseActivity() {
@ -73,9 +65,6 @@ class PhotoEditActivity : BaseActivity() {
private lateinit var filterListFragment: FilterListFragment
private lateinit var editImageFragment: EditImageFragment
private lateinit var viewPager: NonSwipeableViewPager
private lateinit var tabLayout: TabLayout
private var brightnessFinal = BRIGHTNESS_START
private var saturationFinal = SATURATION_START
private var contrastFinal = CONTRAST_START
@ -95,40 +84,40 @@ class PhotoEditActivity : BaseActivity() {
internal var imageUri: Uri? = null
}
private lateinit var binding: ActivityPhotoEditBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_photo_edit)
binding = ActivityPhotoEditBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setTitle(R.string.toolbar_title_edit)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeButtonEnabled(true)
val cropButton: FloatingActionButton = findViewById(R.id.cropImageButton)
initialUri = intent.getParcelableExtra("picture_uri")
imageUri = initialUri
// set on-click listener
cropButton.setOnClickListener {
// Crop button on-click listener
binding.cropImageButton.setOnClickListener {
startCrop()
}
loadImage()
viewPager = findViewById(R.id.viewPager)
tabLayout = findViewById(R.id.tabs)
setupViewPager(viewPager)
tabLayout.setupWithViewPager(viewPager)
setupViewPager(binding.viewPager)
binding.tabs.setupWithViewPager(binding.viewPager)
}
//<editor-fold desc="ON LAUNCH">
private fun loadImage() {
originalImage = MediaStore.Images.Media.getBitmap(contentResolver, imageUri)
compressedImage = resizeImage(originalImage!!)
compressedOriginalImage = compressedImage!!.copy(BITMAP_CONFIG, true)
filteredImage = compressedImage!!.copy(BITMAP_CONFIG, true)
Glide.with(this).load(compressedImage).into(image_preview)
Glide.with(this).load(compressedImage).into(binding.imagePreview)
}
private fun resizeImage(image: Bitmap): Bitmap {
@ -169,49 +158,60 @@ class PhotoEditActivity : BaseActivity() {
when(item.itemId) {
android.R.id.home -> {
onBackPressed()
}
R.id.action_upload -> {
saveImageToGallery(false)
if (noEdits()) onBackPressed()
else {
val builder = AlertDialog.Builder(this)
builder.apply {
setMessage(R.string.save_before_returning)
setPositiveButton(android.R.string.ok) { _, _ ->
saveImageToGallery()
}
setNegativeButton(R.string.no_cancel_edit) { _, _ ->
onBackPressed()
}
}
// Create the AlertDialog
builder.show()
}
}
R.id.action_save -> {
saveImageToGallery(true)
return true
saveImageToGallery()
}
R.id.action_reset -> {
resetControls()
actualFilter = null
imageUri = initialUri
loadImage()
filterListFragment.resetSelectedFilter()
}
}
//<editor-fold desc="FILTERS">
return super.onOptionsItemSelected(item)
}
//</editor-fold>
fun onFilterSelected(filter: Filter) {
resetControls()
filteredImage = compressedOriginalImage!!.copy(BITMAP_CONFIG, true)
image_preview.setImageBitmap(filter.processFilter(filteredImage))
binding.imagePreview.setImageBitmap(filter.processFilter(filteredImage))
compressedImage = filteredImage.copy(BITMAP_CONFIG, true)
actualFilter = filter
resetControls()
}
private fun resetControls() {
editImageFragment.resetControl()
brightnessFinal = BRIGHTNESS_START
saturationFinal = SATURATION_START
contrastFinal = CONTRAST_START
editImageFragment.resetControl()
}
//</editor-fold>
//<editor-fold desc="EDITS">
private fun applyFilterAndShowImage(filter: Filter, image: Bitmap?) {
future?.cancel(true)
future = executor.submit {
val bitmap = filter.processFilter(image!!.copy(BITMAP_CONFIG, true))
image_preview.post {
image_preview.setImageBitmap(bitmap)
binding.imagePreview.post {
binding.imagePreview.setImageBitmap(bitmap)
}
}
}
@ -255,8 +255,6 @@ class PhotoEditActivity : BaseActivity() {
compressedImage = myFilter.processFilter(bitmap)
}
//</editor-fold>
//<editor-fold desc="CROPPING">
private fun startCrop() {
val file = File.createTempFile("temp_crop_img", ".png", cacheDir)
@ -293,8 +291,8 @@ class PhotoEditActivity : BaseActivity() {
val resultCrop: Uri? = UCrop.getOutput(data!!)
if(resultCrop != null) {
imageUri = resultCrop
image_preview.setImageURI(resultCrop)
val bitmap = (image_preview.drawable as BitmapDrawable).bitmap
binding.imagePreview.setImageURI(resultCrop)
val bitmap = (binding.imagePreview.drawable as BitmapDrawable).bitmap
originalImage = bitmap.copy(Bitmap.Config.ARGB_8888, true)
compressedImage = resizeImage(originalImage!!.copy(BITMAP_CONFIG, true))
compressedOriginalImage = compressedImage!!.copy(BITMAP_CONFIG, true)
@ -314,8 +312,6 @@ class PhotoEditActivity : BaseActivity() {
}
}
//</editor-fold>
//<editor-fold desc="FLOW">
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
@ -325,12 +321,9 @@ class PhotoEditActivity : BaseActivity() {
&& grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
// permission was granted
when (requestCode) {
REQUEST_CODE_PERMISSIONS_SAVE_PHOTO -> permissionsGrantedToSave(true)
REQUEST_CODE_PERMISSIONS_SEND_PHOTO -> permissionsGrantedToSave(false)
}
permissionsGrantedToSave()
} else {
Snackbar.make(coordinator_edit, getString(R.string.permission_denied),
Snackbar.make(binding.root, getString(R.string.permission_denied),
Snackbar.LENGTH_LONG).show()
}
}
@ -354,16 +347,16 @@ class PhotoEditActivity : BaseActivity() {
finish()
}
private fun saveImageToGallery(save: Boolean) {
private fun saveImageToGallery() {
// runtime permission and process
if (!allPermissionsGranted()) {
ActivityCompat.requestPermissions(
this,
REQUIRED_PERMISSIONS,
if(save) REQUEST_CODE_PERMISSIONS_SAVE_PHOTO else REQUEST_CODE_PERMISSIONS_SEND_PHOTO
REQUEST_CODE_PERMISSIONS_SEND_PHOTO
)
} else {
permissionsGrantedToSave(save)
permissionsGrantedToSave()
}
}
@ -375,32 +368,6 @@ class PhotoEditActivity : BaseActivity() {
applicationContext, it) == PackageManager.PERMISSION_GRANTED
}
private fun getOutputFile(name: String): Pair<OutputStream, String> {
val outputStream: OutputStream
val path: String
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver: ContentResolver = contentResolver
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
contentValues.put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_PICTURES
)
val imageUri: Uri =
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)!!
path = imageUri.toString()
outputStream = resolver.openOutputStream(Objects.requireNonNull(imageUri))!!
} else {
val imagesDir =
Environment.getExternalStoragePublicDirectory(getString(R.string.app_name))
imagesDir.mkdir()
val file = File(imagesDir, name)
path = Uri.fromFile(file).toString()
outputStream = file.outputStream()
}
return Pair(outputStream, path)
}
private fun OutputStream.writeBitmap(bitmap: Bitmap) {
use { out ->
@ -410,7 +377,13 @@ class PhotoEditActivity : BaseActivity() {
}
}
private fun permissionsGrantedToSave(save: Boolean) {
private fun noEdits(): Boolean =
brightnessFinal == BRIGHTNESS_START
&& contrastFinal == CONTRAST_START
&& saturationFinal == SATURATION_START
&& actualFilter?.let { it.name == getString(R.string.normal_filter)} ?: true
private fun permissionsGrantedToSave() {
if (saving) {
val builder = AlertDialog.Builder(this)
builder.apply {
@ -422,78 +395,37 @@ class PhotoEditActivity : BaseActivity() {
return
}
saving = true
progressBarSaveFile.visibility = VISIBLE
binding.progressBarSaveFile.visibility = VISIBLE
saveFuture = saveExecutor.submit {
val outputStream: OutputStream
var path: String
if (!save) {
//put picture in cache
try {
val path: String
if(!noEdits()) {
// Save modified image in cache
val tempFile = File.createTempFile("temp_edit_img", ".png", cacheDir)
path = Uri.fromFile(tempFile).toString()
outputStream = tempFile.outputStream()
} else {
// Save the picture to gallery
val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis()) + ".png"
val pair = getOutputFile(name)
outputStream = pair.first
path = pair.second
}
try {
if(brightnessFinal != BRIGHTNESS_START || contrastFinal != CONTRAST_START
|| saturationFinal != SATURATION_START
|| (actualFilter != null && actualFilter!!.name != getString(R.string.normal_filter))) {
outputStream.writeBitmap(applyFinalFilters(originalImage))
tempFile.outputStream().writeBitmap(applyFinalFilters(originalImage))
}
else {
if(save) {
contentResolver.openInputStream(imageUri!!)!!.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
path = imageUri.toString()
}
if(saving) {
this.runOnUiThread {
sendBackImage(path)
binding.progressBarSaveFile.visibility = GONE
saving = false
}
else path = imageUri.toString()
}
} catch (e: IOException) {
this.runOnUiThread {
Snackbar.make(
coordinator_edit, getString(R.string.save_image_failed),
binding.root, getString(R.string.save_image_failed),
Snackbar.LENGTH_LONG
).show()
}
}
if(saving) {
this.runOnUiThread {
if(!save) {
sendBackImage(path)
} else {
if(path.startsWith("file")) {
MediaScannerConnection.scanFile(
this,
arrayOf(path.toUri().toFile().absolutePath),
null
) { path, uri ->
if (uri == null) {
Log.e(
"NEW IMAGE SCAN FAILED",
"Tried to scan $path, but it failed"
)
}
}
}
Snackbar.make(
coordinator_edit, getString(R.string.save_image_success),
Snackbar.LENGTH_LONG
).show()
}
progressBarSaveFile.visibility = GONE
binding.progressBarSaveFile.visibility = GONE
saving = false
}
}
}
}
//</editor-fold>
}

View File

@ -2,15 +2,14 @@ package com.h.pixeldroid.postCreation.photoEdit
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ThumbnailListItemBinding
import com.zomato.photofilters.utils.ThumbnailItem
import kotlinx.android.synthetic.main.thumbnail_list_item.view.*
class ThumbnailAdapter (private val context: Context,
private val tbItemList: List<ThumbnailItem>,
@ -18,9 +17,14 @@ class ThumbnailAdapter (private val context: Context,
private var selectedIndex = 0
fun resetSelected(){
selectedIndex = 0
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(context).inflate(R.layout.thumbnail_list_item, parent, false)
return MyViewHolder(itemView)
val itemBinding = ThumbnailListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(itemBinding)
}
override fun getItemCount(): Int {
@ -44,8 +48,8 @@ class ThumbnailAdapter (private val context: Context,
holder.filterName.setTextColor(ContextCompat.getColor(context, R.color.filterLabelNormal))
}
class MyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
var thumbnail: ImageView = itemView.thumbnail
var filterName: TextView = itemView.filter_name
class MyViewHolder(itemBinding: ThumbnailListItemBinding): RecyclerView.ViewHolder(itemBinding.root) {
var thumbnail: ImageView = itemBinding.thumbnail
var filterName: TextView = itemBinding.filterName
}
}

View File

@ -5,13 +5,13 @@ import android.util.Log
import android.view.View
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ActivityPostBinding
import com.h.pixeldroid.utils.api.objects.DiscoverPost
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.api.objects.Status.Companion.DISCOVER_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.DOMAIN_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.utils.BaseActivity
import kotlinx.android.synthetic.main.activity_post.*
import retrofit2.HttpException
import java.io.IOException
@ -20,9 +20,13 @@ class PostActivity : BaseActivity() {
lateinit var domain : String
private lateinit var accessToken : String
private lateinit var binding: ActivityPostBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_post)
binding = ActivityPostBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val status = intent.getSerializableExtra(POST_TAG) as Status?
@ -38,7 +42,7 @@ class PostActivity : BaseActivity() {
arguments.putString(DOMAIN_TAG, domain)
if (discoverPost != null) {
postProgressBar.visibility = View.VISIBLE
binding.postProgressBar.visibility = View.VISIBLE
getDiscoverPost(arguments, discoverPost)
} else {
initializeFragment(arguments, status)
@ -59,7 +63,7 @@ class PostActivity : BaseActivity() {
lifecycleScope.launchWhenCreated {
try {
val status = api.getStatus("Bearer $accessToken", id)
postProgressBar.visibility = View.GONE
binding.postProgressBar.visibility = View.GONE
initializeFragment(arguments, status)
} catch (exception: IOException) {
//TODO show error message
@ -76,6 +80,6 @@ class PostActivity : BaseActivity() {
supportFragmentManager.isStateSaved
supportFragmentManager.beginTransaction()
.add(R.id.postFragmentSingle, postFragment).commit()
postFragmentSingle.visibility = View.VISIBLE
binding.postFragmentSingle.visibility = View.VISIBLE
}
}

View File

@ -5,11 +5,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.PostFragmentBinding
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.api.objects.Status.Companion.DOMAIN_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.utils.BaseFragment
import com.h.pixeldroid.utils.bindingLifecycleAware
class PostFragment : BaseFragment() {
@ -17,30 +18,32 @@ class PostFragment : BaseFragment() {
private lateinit var statusDomain: String
private var currentStatus: Status? = null
var binding: PostFragmentBinding by bindingLifecycleAware()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
currentStatus = arguments?.getSerializable(POST_TAG) as Status?
statusDomain = arguments?.getString(DOMAIN_TAG)!!
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val root: View = inflater.inflate(R.layout.post_fragment, container, false)
binding = PostFragmentBinding.inflate(inflater, container, false)
return binding.root
}
val user = db.userDao().getActiveUser()!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
val holder = StatusViewHolder(root)
val holder = StatusViewHolder(binding)
holder.bind(currentStatus, api, db, lifecycleScope)
return root
}
}

View File

@ -1,25 +1,24 @@
package com.h.pixeldroid.posts
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.api.objects.Report
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.databinding.ActivityReportBinding
import com.h.pixeldroid.utils.BaseActivity
import kotlinx.android.synthetic.main.activity_report.*
import retrofit2.Call
import retrofit2.Callback
import com.h.pixeldroid.utils.api.objects.Status
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
class ReportActivity : BaseActivity() {
private lateinit var binding: ActivityReportBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_report)
binding = ActivityReportBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.report)
@ -29,21 +28,21 @@ class ReportActivity : BaseActivity() {
val user = db.userDao().getActiveUser()
report_target_textview.text = getString(R.string.report_target).format(status?.account?.acct)
binding.reportTargetTextview.text = getString(R.string.report_target).format(status?.account?.acct)
reportButton.setOnClickListener{
reportButton.visibility = View.INVISIBLE
reportProgressBar.visibility = View.VISIBLE
binding.reportButton.setOnClickListener{
binding.reportButton.visibility = View.INVISIBLE
binding.reportProgressBar.visibility = View.VISIBLE
textInputLayout.editText?.isEnabled = false
binding.textInputLayout.editText?.isEnabled = false
val accessToken = user?.accessToken.orEmpty()
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
lifecycleScope.launchWhenCreated {
try {
api.report("Bearer $accessToken", status?.account?.id!!, listOf(status), textInputLayout.editText?.text.toString())
api.report("Bearer $accessToken", status?.account?.id!!, listOf(status), binding.textInputLayout.editText?.text.toString())
reportStatus(true)
} catch (exception: IOException) {
@ -57,15 +56,15 @@ class ReportActivity : BaseActivity() {
private fun reportStatus(success: Boolean){
if(success){
reportProgressBar.visibility = View.GONE
reportButton.isEnabled = false
reportButton.text = getString(R.string.reported)
reportButton.visibility = View.VISIBLE
binding.reportProgressBar.visibility = View.GONE
binding.reportButton.isEnabled = false
binding.reportButton.text = getString(R.string.reported)
binding.reportButton.visibility = View.VISIBLE
} else {
textInputLayout.error = getString(R.string.report_error)
reportButton.visibility = View.VISIBLE
textInputLayout.editText?.isEnabled = true
reportProgressBar.visibility = View.GONE
binding.textInputLayout.error = getString(R.string.report_error)
binding.reportButton.visibility = View.VISIBLE
binding.textInputLayout.editText?.isEnabled = true
binding.reportProgressBar.visibility = View.GONE
}
}

View File

@ -16,60 +16,32 @@ import android.widget.*
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import at.connyduck.sparkbutton.SparkButton
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.AlbumImageViewBinding
import com.h.pixeldroid.databinding.CommentBinding
import com.h.pixeldroid.databinding.PostFragmentBinding
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Attachment
import com.h.pixeldroid.utils.api.objects.Context
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.db.AppDatabase
import com.karumi.dexter.Dexter
import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse
import com.karumi.dexter.listener.single.BasePermissionListener
import kotlinx.android.synthetic.main.comment.view.*
import kotlinx.android.synthetic.main.post_fragment.view.*
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
/**
* View Holder for a [Status] RecyclerView list item.
*/
class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val profilePic : ImageView = view.findViewById(R.id.profilePic)
val postPic : ImageView = view.findViewById(R.id.postPicture)
val username : TextView = view.findViewById(R.id.username)
val usernameDesc: TextView = view.findViewById(R.id.usernameDesc)
val description : TextView = view.findViewById(R.id.description)
val nlikes : TextView = view.findViewById(R.id.nlikes)
val nshares : TextView = view.findViewById(R.id.nshares)
//Spark buttons
val liker : SparkButton = view.findViewById(R.id.liker)
val reblogger : SparkButton = view.findViewById(R.id.reblogger)
val submitCmnt : ImageButton = view.findViewById(R.id.submitComment)
val commenter : ImageView = view.findViewById(R.id.commenter)
val comment : EditText = view.findViewById(R.id.editComment)
val commentCont : LinearLayout = view.findViewById(R.id.commentContainer)
val commentIn : LinearLayout = view.findViewById(R.id.commentIn)
val viewComment : TextView = view.findViewById(R.id.ViewComments)
val postDate : TextView = view.findViewById(R.id.postDate)
val postDomain : TextView = view.findViewById(R.id.postDomain)
val sensitiveW : TextView = view.findViewById(R.id.sensitiveWarning)
val postPager : ViewPager2 = view.findViewById(R.id.postPager)
val more : ImageButton = view.findViewById(R.id.status_more)
class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHolder(binding.root) {
private var status: Status? = null
@ -80,7 +52,7 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val metrics = itemView.context.resources.displayMetrics
//Limit the height of the different images
postPic.maxHeight = metrics.heightPixels * 3/4
binding.postPicture.maxHeight = metrics.heightPixels * 3/4
//Setup the post layout
val picRequest = Glide.with(itemView)
@ -89,37 +61,35 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val user = db.userDao().getActiveUser()!!
setupPost(itemView, picRequest, user.instance_uri, false)
setupPost(picRequest, user.instance_uri, false)
activateButtons(this, pixelfedAPI, db, lifecycleScope)
activateButtons(pixelfedAPI, db, lifecycleScope)
}
private fun setupPost(
rootView: View,
request: RequestBuilder<Drawable>,
//homeFragment: Fragment,
domain: String,
isActivity: Boolean
) {
//Setup username as a button that opens the profile
rootView.findViewById<TextView>(R.id.username).apply {
binding.username.apply {
text = status?.account?.getDisplayName() ?: ""
setTypeface(null, Typeface.BOLD)
setOnClickListener { status?.account?.openProfile(rootView.context) }
}
rootView.findViewById<TextView>(R.id.usernameDesc).apply {
binding.usernameDesc.apply {
text = status?.account?.getDisplayName() ?: ""
setTypeface(null, Typeface.BOLD)
}
rootView.findViewById<TextView>(R.id.nlikes).apply {
binding.nlikes.apply {
text = status?.getNLikes(rootView.context)
setTypeface(null, Typeface.BOLD)
}
rootView.findViewById<TextView>(R.id.nshares).apply {
binding.nshares.apply {
text = status?.getNShares(rootView.context)
setTypeface(null, Typeface.BOLD)
}
@ -127,82 +97,81 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
//Convert the date to a readable string
setTextViewFromISO8601(
status?.created_at!!,
rootView.postDate,
binding.postDate,
isActivity,
rootView.context
binding.root.context
)
rootView.postDomain.text = status?.getStatusDomain(domain)
binding.postDomain.text = status?.getStatusDomain(domain)
//Setup images
ImageConverter.setRoundImageFromURL(
rootView,
binding.root,
status?.getProfilePicUrl(),
rootView.profilePic
binding.profilePic
)
rootView.profilePic.setOnClickListener { status?.account?.openProfile(rootView.context) }
binding.profilePic.setOnClickListener { status?.account?.openProfile(binding.root.context) }
//Setup post pic only if there are media attachments
if(!status?.media_attachments.isNullOrEmpty()) {
setupPostPics(rootView, request)
setupPostPics(binding, request)
} else {
rootView.postPicture.visibility = View.GONE
rootView.postPager.visibility = View.GONE
rootView.postTabs.visibility = View.GONE
binding.postPicture.visibility = View.GONE
binding.postPager.visibility = View.GONE
binding.postTabs.visibility = View.GONE
}
//Set comment initial visibility
rootView.findViewById<LinearLayout>(R.id.commentIn).visibility = View.GONE
rootView.findViewById<LinearLayout>(R.id.commentContainer).visibility = View.GONE
binding.commentIn.visibility = View.GONE
binding.commentContainer.visibility = View.GONE
}
private fun setupPostPics(
rootView: View,
binding: PostFragmentBinding,
request: RequestBuilder<Drawable>,
//homeFragment: Fragment
) {
// Standard layout
rootView.postPicture.visibility = View.VISIBLE
rootView.postPager.visibility = View.GONE
rootView.postTabs.visibility = View.GONE
binding.postPicture.visibility = View.VISIBLE
binding.postPager.visibility = View.GONE
binding.postTabs.visibility = View.GONE
if(status?.media_attachments?.size == 1) {
request.load(status?.getPostUrl()).into(rootView.postPicture)
val imgDescription = status?.media_attachments?.get(0)?.description.orEmpty().ifEmpty { rootView.context.getString(
request.load(status?.getPostUrl()).into(binding.postPicture)
val imgDescription = status?.media_attachments?.get(0)?.description.orEmpty().ifEmpty { binding.root.context.getString(
R.string.no_description) }
rootView.postPicture.contentDescription = imgDescription
binding.postPicture.contentDescription = imgDescription
rootView.postPicture.setOnLongClickListener {
binding.postPicture.setOnLongClickListener {
Snackbar.make(it, imgDescription, Snackbar.LENGTH_SHORT).show()
true
}
} else if(status?.media_attachments?.size!! > 1) {
setupTabsLayout(rootView, request)
setupTabsLayout(binding, request)
}
if (status?.sensitive!!) {
status?.setupSensitiveLayout(rootView)
status?.setupSensitiveLayout(binding)
}
}
private fun setupTabsLayout(
rootView: View,
binding: PostFragmentBinding,
request: RequestBuilder<Drawable>,
) {
//Only show the viewPager and tabs
rootView.postPicture.visibility = View.GONE
rootView.postPager.visibility = View.VISIBLE
rootView.postTabs.visibility = View.VISIBLE
binding.postPicture.visibility = View.GONE
binding.postPager.visibility = View.VISIBLE
binding.postTabs.visibility = View.VISIBLE
//Attach the given tabs to the view pager
rootView.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList())
binding.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList())
TabLayoutMediator(rootView.postTabs, rootView.postPager) { tab, _ ->
tab.icon = ContextCompat.getDrawable(rootView.context, R.drawable.ic_dot_blue_12dp)
TabLayoutMediator(binding.postTabs, binding.postPager) { tab, _ ->
tab.icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_dot_blue_12dp)
}.attach()
}
@ -212,7 +181,7 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
credential: String,
lifecycleScope: LifecycleCoroutineScope
) {
rootView.findViewById<TextView>(R.id.description).apply {
binding.description.apply {
if (status?.content.isNullOrBlank()) {
visibility = View.GONE
} else {
@ -229,39 +198,36 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}
}
private fun activateButtons(holder: StatusViewHolder, api: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){
private fun activateButtons(api: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){
val user = db.userDao().getActiveUser()!!
val credential = "Bearer ${user.accessToken}"
//Set the special HTML text
setDescription(holder.view, api, credential, lifecycleScope)
setDescription(binding.root, api, credential, lifecycleScope)
//Activate onclickListeners
activateLiker(
holder, api, credential,
status?.favourited ?: false,
lifecycleScope
api, credential, status?.favourited ?: false,
lifecycleScope
)
activateReblogger(
holder, api, credential,
status?.reblogged ?: false,
lifecycleScope
api, credential, status?.reblogged ?: false,
lifecycleScope
)
activateCommenter(holder, api, credential, lifecycleScope)
activateCommenter(api, credential, lifecycleScope)
showComments(holder, api, credential, lifecycleScope)
showComments(api, credential, lifecycleScope)
activateMoreButton(holder, api, db, lifecycleScope)
activateMoreButton(api, db, lifecycleScope)
}
private fun activateReblogger(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
isReblogged: Boolean,
lifecycleScope: LifecycleCoroutineScope
) {
holder.reblogger.apply {
binding.reblogger.apply {
//Set initial button state
isChecked = isReblogged
@ -270,10 +236,10 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
lifecycleScope.launchWhenCreated {
if (buttonState) {
// Button is active
undoReblogPost(holder, api, credential)
undoReblogPost(api, credential)
} else {
// Button is inactive
reblogPost(holder, api, credential)
reblogPost(api, credential)
}
}
//show animation or not?
@ -283,7 +249,6 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}
private suspend fun reblogPost(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String
) {
@ -294,20 +259,19 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val resp = api.reblogStatus(credential, it)
//Update shown share count
holder.nshares.text = resp.getNShares(holder.view.context)
holder.reblogger.isChecked = resp.reblogged!!
binding.nshares.text = resp.getNShares(binding.root.context)
binding.reblogger.isChecked = resp.reblogged!!
} catch (exception: IOException) {
Log.e("REBLOG ERROR", exception.toString())
holder.reblogger.isChecked = false
binding.reblogger.isChecked = false
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
holder.reblogger.isChecked = false
binding.reblogger.isChecked = false
}
}
}
private suspend fun undoReblogPost(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
@ -317,20 +281,20 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val resp = api.undoReblogStatus(credential, it)
//Update shown share count
holder.nshares.text = resp.getNShares(holder.view.context)
holder.reblogger.isChecked = resp.reblogged!!
binding.nshares.text = resp.getNShares(binding.root.context)
binding.reblogger.isChecked = resp.reblogged!!
} catch (exception: IOException) {
Log.e("REBLOG ERROR", exception.toString())
holder.reblogger.isChecked = true
binding.reblogger.isChecked = true
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
holder.reblogger.isChecked = true
binding.reblogger.isChecked = true
}
}
}
private fun activateMoreButton(holder: StatusViewHolder, api: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){
holder.more.setOnClickListener {
private fun activateMoreButton(api: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){
binding.statusMore.setOnClickListener {
PopupMenu(it.context, it).apply {
setOnMenuItemClickListener { item ->
when (item.itemId) {
@ -354,46 +318,46 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
true
}
R.id.post_more_menu_save_to_gallery -> {
Dexter.withContext(holder.view.context)
Dexter.withContext(binding.root.context)
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.withListener(object : BasePermissionListener() {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
Toast.makeText(
holder.view.context,
holder.view.context.getString(R.string.write_permission_download_pic),
binding.root.context,
binding.root.context.getString(R.string.write_permission_download_pic),
Toast.LENGTH_SHORT
).show()
}
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
status?.downloadImage(
holder.view.context,
status?.media_attachments?.get(holder.postPager.currentItem)?.url
binding.root.context,
status?.media_attachments?.get(binding.postPager.currentItem)?.url
?: "",
holder.view
binding.root
)
}
}).check()
true
}
R.id.post_more_menu_share_picture -> {
Dexter.withContext(holder.view.context)
Dexter.withContext(binding.root.context)
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.withListener(object : BasePermissionListener() {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
Toast.makeText(
holder.view.context,
holder.view.context.getString(R.string.write_permission_share_pic),
binding.root.context,
binding.root.context.getString(R.string.write_permission_share_pic),
Toast.LENGTH_SHORT
).show()
}
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
status?.downloadImage(
holder.view.context,
status?.media_attachments?.get(holder.postPager.currentItem)?.url
binding.root.context,
status?.media_attachments?.get(binding.postPager.currentItem)?.url
?: "",
holder.view,
binding.root,
share = true,
)
}
@ -401,10 +365,10 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
true
}
R.id.post_more_menu_delete -> {
val builder = AlertDialog.Builder(holder.itemView.context)
val builder = AlertDialog.Builder(binding.root.context)
builder.apply {
setMessage(R.string.delete_dialog)
setPositiveButton(R.string.OK) { _, _ ->
setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch {
val user = db.userDao().getActiveUser()!!
@ -413,14 +377,14 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
db.publicPostDao().delete(id, user.user_id, user.instance_uri)
try {
api.deleteStatus("Bearer ${user.accessToken}", id)
holder.itemView.visibility = View.GONE
binding.root.visibility = View.GONE
} catch (exception: IOException) {
} catch (exception: HttpException) {
}
}
}
}
setNegativeButton(R.string.cancel) { _, _ -> }
setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
}
@ -446,14 +410,13 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}
private fun activateLiker(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
isLiked: Boolean,
lifecycleScope: LifecycleCoroutineScope
) {
holder.liker.apply {
binding.liker.apply {
//Set initial state
isChecked = isLiked
@ -462,10 +425,10 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
lifecycleScope.launchWhenCreated {
if (buttonState) {
// Button is active, unlike
unLikePostCall(holder, api, credential)
unLikePostCall(api, credential)
} else {
// Button is inactive, like
likePostCall(holder, api, credential)
likePostCall(api, credential)
}
}
//show animation or not?
@ -474,38 +437,36 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}
//Activate double tap liking
holder.apply {
var clicked = false
postPic.setOnClickListener {
lifecycleScope.launchWhenCreated {
//Check that the post isn't hidden
if(sensitiveW.visibility == View.GONE) {
//Check for double click
if(clicked) {
if (holder.liker.isChecked) {
// Button is active, unlike
holder.liker.isChecked = false
unLikePostCall(holder, api, credential)
} else {
// Button is inactive, like
holder.liker.playAnimation()
holder.liker.isChecked = true
likePostCall(holder, api, credential)
}
var clicked = false
binding.postPicture.setOnClickListener {
lifecycleScope.launchWhenCreated {
//Check that the post isn't hidden
if(binding.sensitiveWarning.visibility == View.GONE) {
//Check for double click
if(clicked) {
if (binding.liker.isChecked) {
// Button is active, unlike
binding.liker.isChecked = false
unLikePostCall(api, credential)
} else {
clicked = true
//Reset clicked to false after 500ms
postPic.handler.postDelayed(fun() { clicked = false }, 500)
// Button is inactive, like
binding.liker.playAnimation()
binding.liker.isChecked = true
likePostCall(api, credential)
}
} else {
clicked = true
//Reset clicked to false after 500ms
binding.postPicture.handler.postDelayed(fun() { clicked = false }, 500)
}
}
}
}
}
private suspend fun likePostCall(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
@ -516,20 +477,19 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val resp = api.likePost(credential, it)
//Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes(holder.view.context)
holder.liker.isChecked = resp.favourited ?: false
binding.nlikes.text = resp.getNLikes(binding.root.context)
binding.liker.isChecked = resp.favourited ?: false
} catch (exception: IOException) {
Log.e("LIKE ERROR", exception.toString())
holder.liker.isChecked = false
binding.liker.isChecked = false
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
holder.liker.isChecked = false
binding.liker.isChecked = false
}
}
}
private suspend fun unLikePostCall(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
@ -540,37 +500,36 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val resp = api.unlikePost(credential, it)
//Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes(holder.view.context)
holder.liker.isChecked = resp.favourited ?: false
binding.nlikes.text = resp.getNLikes(binding.root.context)
binding.liker.isChecked = resp.favourited ?: false
} catch (exception: IOException) {
Log.e("UNLIKE ERROR", exception.toString())
holder.liker.isChecked = true
binding.liker.isChecked = true
} catch (exception: HttpException) {
Log.e("RESPONSE_CODE", exception.code().toString())
holder.liker.isChecked = true
binding.liker.isChecked = true
}
}
}
private fun showComments(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
lifecycleScope: LifecycleCoroutineScope
) {
//Show all comments of a post
if (status?.replies_count == 0) {
holder.viewComment.text = holder.view.context.getString(R.string.NoCommentsToShow)
binding.viewComments.text = binding.root.context.getString(R.string.NoCommentsToShow)
} else {
holder.viewComment.apply {
text = holder.view.context.getString(R.string.number_comments)
binding.viewComments.apply {
text = binding.root.context.getString(R.string.number_comments)
.format(status?.replies_count)
setOnClickListener {
visibility = View.GONE
lifecycleScope.launchWhenCreated {
//Retrieve the comments
retrieveComments(holder, api, credential)
retrieveComments(api, credential)
}
}
}
@ -578,52 +537,49 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}
private fun activateCommenter(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
lifecycleScope: LifecycleCoroutineScope
) {
//Toggle comment button
toggleCommentInput(holder)
toggleCommentInput()
//Activate commenterpostPicture
holder.submitCmnt.setOnClickListener {
val textIn = holder.comment.text
binding.submitComment.setOnClickListener {
val textIn = binding.editComment.text
//Open text input
if(textIn.isNullOrEmpty()) {
Toast.makeText(
holder.view.context,
holder.view.context.getString(R.string.empty_comment),
binding.root.context,
binding.root.context.getString(R.string.empty_comment),
Toast.LENGTH_SHORT
).show()
} else {
//Post the comment
lifecycleScope.launchWhenCreated {
postComment(holder, api, credential)
postComment(api, credential)
}
}
}
}
private fun toggleCommentInput(
holder : StatusViewHolder
) {
private fun toggleCommentInput() {
//Toggle comment button
holder.commenter.setOnClickListener {
when(holder.commentIn.visibility) {
binding.commenter.setOnClickListener {
when(binding.commentIn.visibility) {
View.VISIBLE -> {
holder.commentIn.visibility = View.GONE
binding.commentIn.visibility = View.GONE
ImageConverter.setImageFromDrawable(
holder.view,
holder.commenter,
binding.root,
binding.commenter,
R.drawable.ic_comment_empty
)
}
View.GONE -> {
holder.commentIn.visibility = View.VISIBLE
binding.commentIn.visibility = View.VISIBLE
ImageConverter.setImageFromDrawable(
holder.view,
holder.commenter,
binding.root,
binding.commenter,
R.drawable.ic_comment_blue
)
}
@ -633,15 +589,16 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun addComment(context: android.content.Context, commentContainer: LinearLayout, commentUsername: String, commentContent: String) {
val view = LayoutInflater.from(context)
.inflate(R.layout.comment, commentContainer, true)
view.user.text = commentUsername
view.commentText.text = commentContent
val itemBinding = CommentBinding.inflate(
LayoutInflater.from(context), commentContainer, false
)
itemBinding.user.text = commentUsername
itemBinding.commentText.text = commentContent
}
private suspend fun retrieveComments(
holder: StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
@ -649,15 +606,15 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
try {
val statuses = api.statusComments(it, credential).descendants
holder.commentCont.removeAllViews()
binding.commentContainer.removeAllViews()
//Create the new views for each comment
for (status in statuses) {
addComment(holder.view.context, holder.commentCont, status.account!!.username!!,
addComment(binding.root.context, binding.commentContainer, status.account!!.username!!,
status.content!!
)
}
holder.commentCont.visibility = View.VISIBLE
binding.commentContainer.visibility = View.VISIBLE
} catch (exception: IOException) {
Log.e("COMMENT FETCH ERROR", exception.toString())
@ -668,37 +625,36 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
}
private suspend fun postComment(
holder : StatusViewHolder,
api: PixelfedAPI,
credential: String,
) {
val textIn = holder.comment.text
val textIn = binding.editComment.text
val nonNullText = textIn.toString()
status?.id?.let {
try {
val response = api.postStatus(credential, nonNullText, it)
holder.commentIn.visibility = View.GONE
binding.commentIn.visibility = View.GONE
//Add the comment to the comment section
addComment(
holder.view.context, holder.commentCont, response.account!!.username!!,
binding.root.context, binding.commentContainer, response.account!!.username!!,
response.content!!
)
Toast.makeText(
holder.view.context,
holder.view.context.getString(R.string.comment_posted).format(textIn),
binding.root.context,
binding.root.context.getString(R.string.comment_posted).format(textIn),
Toast.LENGTH_SHORT
).show()
} catch (exception: IOException) {
Log.e("COMMENT ERROR", exception.toString())
Toast.makeText(
holder.view.context, holder.view.context.getString(R.string.comment_error),
binding.root.context, binding.root.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
holder.view.context, holder.view.context.getString(R.string.comment_error),
binding.root.context, binding.root.context.getString(R.string.comment_error),
Toast.LENGTH_SHORT
).show()
Log.e("ERROR_CODE", exception.code().toString())
@ -709,26 +665,32 @@ class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
companion object {
fun create(parent: ViewGroup): StatusViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.post_fragment, parent, false)
return StatusViewHolder(view)
val itemBinding = PostFragmentBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return StatusViewHolder(itemBinding)
}
}
}
class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) : RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) :
RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.album_image_view, parent, false))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemBinding = AlbumImageViewBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(itemBinding)
}
override fun getItemCount() = media_attachments.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
Glide.with(holder.view)
Glide.with(holder.binding.root)
.asDrawable().fitCenter().placeholder(ColorDrawable(Color.GRAY))
.load(media_attachments[position].url).into(holder.image)
val description = media_attachments[position].description
.orEmpty().ifEmpty{ holder.view.context.getString(R.string.no_description)}
.orEmpty().ifEmpty{ holder.binding.root.context.getString(R.string.no_description)}
holder.image.setOnLongClickListener {
Snackbar.make(it, description, Snackbar.LENGTH_SHORT).show()
@ -738,7 +700,7 @@ class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) : R
holder.image.contentDescription = description
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view){
val image: ImageView = view.findViewById(R.id.imageImageView)
class ViewHolder(val binding: AlbumImageViewBinding) : RecyclerView.ViewHolder(binding.root){
val image: ImageView = binding.imageImageView
}
}

View File

@ -18,22 +18,21 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.h.pixeldroid.posts.PostActivity
import com.h.pixeldroid.profile.ProfileActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.di.PixelfedAPIHolder
import com.h.pixeldroid.utils.api.objects.Account
import com.h.pixeldroid.utils.api.objects.Notification
import com.h.pixeldroid.utils.api.objects.Status
import kotlinx.android.synthetic.main.fragment_notifications.view.*
import com.h.pixeldroid.databinding.FragmentNotificationsBinding
import com.h.pixeldroid.posts.PostActivity
import com.h.pixeldroid.posts.feeds.cachedFeeds.CachedFeedFragment
import com.h.pixeldroid.posts.feeds.cachedFeeds.FeedViewModel
import com.h.pixeldroid.posts.feeds.cachedFeeds.ViewModelFactory
import com.h.pixeldroid.posts.parseHTMLText
import com.h.pixeldroid.posts.setTextViewFromISO8601
import com.h.pixeldroid.profile.ProfileActivity
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Account
import com.h.pixeldroid.utils.api.objects.Notification
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.di.PixelfedAPIHolder
/**
@ -71,12 +70,12 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
/**
* View Holder for a [Notification] RecyclerView list item.
*/
class NotificationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val notificationType: TextView = view.notification_type
private val notificationTime: TextView = view.notification_time
private val postDescription: TextView = view.notification_post_description
private val avatar: ImageView = view.notification_avatar
private val photoThumbnail: ImageView = view.notification_photo_thumbnail
class NotificationViewHolder(binding: FragmentNotificationsBinding) : RecyclerView.ViewHolder(binding.root) {
private val notificationType: TextView = binding.notificationType
private val notificationTime: TextView = binding.notificationTime
private val postDescription: TextView = binding.notificationPostDescription
private val avatar: ImageView = binding.notificationAvatar
private val photoThumbnail: ImageView = binding.notificationPhotoThumbnail
private var notification: Notification? = null
@ -216,9 +215,10 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
companion object {
fun create(parent: ViewGroup): NotificationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_notifications, parent, false)
return NotificationViewHolder(view)
val itemBinding = FragmentNotificationsBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return NotificationViewHolder(itemBinding)
}
}
}

View File

@ -14,13 +14,13 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.AccountListEntryBinding
import com.h.pixeldroid.posts.feeds.uncachedFeeds.FeedViewModel
import com.h.pixeldroid.posts.feeds.uncachedFeeds.UncachedFeedFragment
import com.h.pixeldroid.posts.feeds.uncachedFeeds.ViewModelFactory
import com.h.pixeldroid.utils.api.objects.Account
import com.h.pixeldroid.utils.api.objects.Account.Companion.ACCOUNT_ID_TAG
import com.h.pixeldroid.utils.api.objects.Account.Companion.FOLLOWERS_TAG
import kotlinx.android.synthetic.main.account_list_entry.view.*
/**
@ -75,10 +75,10 @@ class AccountListFragment : UncachedFeedFragment<Account>() {
/**
* View Holder for an [Account] RecyclerView list item.
*/
class AccountViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val avatar : ImageView = view.account_entry_avatar
private val username : TextView = view.account_entry_username
private val acct: TextView = view.account_entry_acct
class AccountViewHolder(binding: AccountListEntryBinding) : RecyclerView.ViewHolder(binding.root) {
private val avatar : ImageView = binding.accountEntryAvatar
private val username : TextView = binding.accountEntryUsername
private val acct: TextView = binding.accountEntryAcct
private var account: Account? = null
@ -104,9 +104,10 @@ class AccountViewHolder(view: View) : RecyclerView.ViewHolder(view) {
companion object {
fun create(parent: ViewGroup): AccountViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.account_list_entry, parent, false)
return AccountViewHolder(view)
val itemBinding = AccountListEntryBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return AccountViewHolder(itemBinding)
}
}
}

View File

@ -12,12 +12,12 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.h.pixeldroid.R
import com.h.pixeldroid.posts.feeds.uncachedFeeds.UncachedFeedFragment
import com.h.pixeldroid.databinding.FragmentTagsBinding
import com.h.pixeldroid.posts.feeds.uncachedFeeds.FeedViewModel
import com.h.pixeldroid.posts.feeds.uncachedFeeds.UncachedFeedFragment
import com.h.pixeldroid.posts.feeds.uncachedFeeds.ViewModelFactory
import com.h.pixeldroid.utils.api.objects.Results
import com.h.pixeldroid.utils.api.objects.Tag
import kotlinx.android.synthetic.main.fragment_tags.view.*
/**
* Fragment to show a list of [hashtag][Tag]s, as a result of a search.
@ -100,9 +100,9 @@ class HashTagAdapter : PagingDataAdapter<Tag, RecyclerView.ViewHolder>(
/**
* View Holder for a [Tag] RecyclerView list item.
*/
class HashTagViewHolder(view: View) : RecyclerView.ViewHolder(view) {
class HashTagViewHolder(binding: FragmentTagsBinding) : RecyclerView.ViewHolder(binding.root) {
private val name : TextView = view.tag_name
private val name : TextView = binding.tagName
private var tag: Tag? = null
@ -124,9 +124,10 @@ class HashTagViewHolder(view: View) : RecyclerView.ViewHolder(view) {
companion object {
fun create(parent: ViewGroup): HashTagViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_tags, parent, false)
return HashTagViewHolder(view)
val itemBinding = FragmentTagsBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return HashTagViewHolder(itemBinding)
}
}
}

View File

@ -14,14 +14,14 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.utils.api.objects.Account
import com.h.pixeldroid.utils.api.objects.Relationship
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.databinding.ActivityProfileBinding
import com.h.pixeldroid.posts.parseHTMLText
import com.h.pixeldroid.utils.BaseActivity
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Account
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.utils.openUrl
import kotlinx.coroutines.launch
import retrofit2.Call
@ -33,16 +33,17 @@ import java.io.IOException
class ProfileActivity : BaseActivity() {
private lateinit var pixelfedAPI : PixelfedAPI
private lateinit var adapter : ProfilePostsRecyclerViewAdapter
private lateinit var recycler : RecyclerView
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var accessToken : String
private lateinit var domain : String
private var user: UserDatabaseEntity? = null
private lateinit var binding: ActivityProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_profile)
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
user = db.userDao().getActiveUser()
@ -52,19 +53,16 @@ class ProfileActivity : BaseActivity() {
accessToken = user?.accessToken.orEmpty()
// Set posts RecyclerView as a grid with 3 columns
recycler = findViewById(R.id.profilePostsRecyclerView)
recycler.layoutManager = GridLayoutManager(applicationContext, 3)
binding.profilePostsRecyclerView.layoutManager = GridLayoutManager(applicationContext, 3)
adapter = ProfilePostsRecyclerViewAdapter()
recycler.adapter = adapter
binding.profilePostsRecyclerView.adapter = adapter
// Set profile according to given account
val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account?
setContent(account)
refreshLayout = findViewById(R.id.profileRefreshLayout)
refreshLayout.setOnRefreshListener {
binding.profileRefreshLayout.setOnRefreshListener {
getAndSetAccount(account?.id ?: user!!.user_id)
}
}
@ -101,9 +99,9 @@ class ProfileActivity : BaseActivity() {
// On click open followers list
findViewById<TextView>(R.id.nbFollowersTextView).setOnClickListener{ onClickFollowers(account) }
binding.nbFollowersTextView.setOnClickListener{ onClickFollowers(account) }
// On click open followers list
findViewById<TextView>(R.id.nbFollowingTextView).setOnClickListener{ onClickFollowing(account) }
binding.nbFollowingTextView.setOnClickListener{ onClickFollowing(account) }
}
private fun getAndSetAccount(id: String){
@ -121,59 +119,50 @@ class ProfileActivity : BaseActivity() {
}
private fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){
val motionLayout = findViewById<MotionLayout>(R.id.motionLayout)
val motionLayout = binding.motionLayout
if(show){
motionLayout?.transitionToEnd()
motionLayout.transitionToEnd()
} else {
motionLayout?.transitionToStart()
motionLayout.transitionToStart()
}
findViewById<ProgressBar>(R.id.profileProgressBar).visibility = View.GONE
refreshLayout.isRefreshing = false
binding.profileProgressBar.visibility = View.GONE
binding.profileRefreshLayout.isRefreshing = false
}
/**
* Populate profile page with user's data
*/
private fun setViews(account: Account) {
val profilePicture = findViewById<ImageView>(R.id.profilePictureImageView)
val profilePicture = binding.profilePictureImageView
ImageConverter.setRoundImageFromURL(
View(applicationContext),
account.avatar,
profilePicture
)
val description = findViewById<TextView>(R.id.descriptionTextView)
description.text = parseHTMLText(
binding.descriptionTextView.text = parseHTMLText(
account.note ?: "", emptyList(), pixelfedAPI,
applicationContext, "Bearer $accessToken",
lifecycleScope
)
val accountName = findViewById<TextView>(R.id.accountNameTextView)
accountName.text = account.getDisplayName()
val displayName = account.getDisplayName()
binding.accountNameTextView.text = displayName
supportActionBar?.title = displayName
if(displayName != "@${account.acct}"){
supportActionBar?.subtitle = "@${account.acct}"
}
accountName.setTypeface(null, Typeface.BOLD)
val nbPosts = findViewById<TextView>(R.id.nbPostsTextView)
nbPosts.text = applicationContext.getString(R.string.nb_posts)
binding.nbPostsTextView.text = applicationContext.getString(R.string.nb_posts)
.format(account.statuses_count.toString())
nbPosts.setTypeface(null, Typeface.BOLD)
val nbFollowers = findViewById<TextView>(R.id.nbFollowersTextView)
nbFollowers.text = applicationContext.getString(R.string.nb_followers)
binding.nbFollowersTextView.text = applicationContext.getString(R.string.nb_followers)
.format(account.followers_count.toString())
nbFollowers.setTypeface(null, Typeface.BOLD)
val nbFollowing = findViewById<TextView>(R.id.nbFollowingTextView)
nbFollowing.text = applicationContext.getString(R.string.nb_following)
binding.nbFollowingTextView.text = applicationContext.getString(R.string.nb_following)
.format(account.following_count.toString())
nbFollowing.setTypeface(null, Typeface.BOLD)
}
/**
@ -227,9 +216,10 @@ class ProfileActivity : BaseActivity() {
private fun activateEditButton() {
// Edit button redirects to Pixelfed's "edit account" page
val editButton = findViewById<Button>(R.id.editButton)
editButton.visibility = View.VISIBLE
editButton.setOnClickListener{ onClickEditButton() }
binding.editButton.apply {
visibility = View.VISIBLE
setOnClickListener{ onClickEditButton() }
}
}
/**
@ -244,14 +234,12 @@ class ProfileActivity : BaseActivity() {
).firstOrNull()
if(relationship != null){
val followButton = findViewById<Button>(R.id.followButton)
if (relationship.following) {
setOnClickUnfollow(account)
} else {
setOnClickFollow(account)
}
followButton.visibility = View.VISIBLE
binding.followButton.visibility = View.VISIBLE
}
} catch (exception: IOException) {
Log.e("FOLLOW ERROR", exception.toString())
@ -269,53 +257,51 @@ class ProfileActivity : BaseActivity() {
}
private fun setOnClickFollow(account: Account) {
val followButton = findViewById<Button>(R.id.followButton)
followButton.setText(R.string.follow)
followButton.setOnClickListener {
lifecycleScope.launchWhenResumed {
try {
pixelfedAPI.follow(account.id.orEmpty(), "Bearer $accessToken")
setOnClickUnfollow(account)
} catch (exception: IOException) {
Log.e("FOLLOW ERROR", exception.toString())
Toast.makeText(
applicationContext, getString(R.string.follow_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
applicationContext, getString(R.string.follow_error),
Toast.LENGTH_SHORT
).show()
binding.followButton.apply {
setText(R.string.follow)
setOnClickListener {
lifecycleScope.launchWhenResumed {
try {
pixelfedAPI.follow(account.id.orEmpty(), "Bearer $accessToken")
setOnClickUnfollow(account)
} catch (exception: IOException) {
Log.e("FOLLOW ERROR", exception.toString())
Toast.makeText(
applicationContext, getString(R.string.follow_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
applicationContext, getString(R.string.follow_error),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
private fun setOnClickUnfollow(account: Account) {
val followButton = findViewById<Button>(R.id.followButton)
binding.followButton.apply {
setText(R.string.unfollow)
followButton.setText(R.string.unfollow)
followButton.setOnClickListener {
lifecycleScope.launchWhenResumed {
try {
pixelfedAPI.unfollow(account.id.orEmpty(), "Bearer $accessToken")
setOnClickFollow(account)
} catch (exception: IOException) {
Log.e("FOLLOW ERROR", exception.toString())
Toast.makeText(
applicationContext, getString(R.string.unfollow_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
applicationContext, getString(R.string.unfollow_error),
Toast.LENGTH_SHORT
).show()
setOnClickListener {
lifecycleScope.launchWhenResumed {
try {
pixelfedAPI.unfollow(account.id.orEmpty(), "Bearer $accessToken")
setOnClickFollow(account)
} catch (exception: IOException) {
Log.e("FOLLOW ERROR", exception.toString())
Toast.makeText(
applicationContext, getString(R.string.unfollow_error),
Toast.LENGTH_SHORT
).show()
} catch (exception: HttpException) {
Toast.makeText(
applicationContext, getString(R.string.unfollow_error),
Toast.LENGTH_SHORT
).show()
}
}
}
}

View File

@ -17,6 +17,8 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.FragmentSearchBinding
import com.h.pixeldroid.databinding.PostFragmentBinding
import com.h.pixeldroid.profile.ProfilePostViewHolder
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.DiscoverPost
@ -25,6 +27,7 @@ import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.posts.PostActivity
import com.h.pixeldroid.utils.BaseFragment
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.bindingLifecycleAware
import com.mikepenz.iconics.IconicsColor
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -46,37 +49,35 @@ class SearchDiscoverFragment : BaseFragment() {
private lateinit var recycler : RecyclerView
private lateinit var adapter : DiscoverRecyclerViewAdapter
private lateinit var accessToken: String
private lateinit var discoverProgressBar: ProgressBar
private lateinit var discoverRefreshLayout: SwipeRefreshLayout
var binding: FragmentSearchBinding by bindingLifecycleAware()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_search, container, false)
val search = view.findViewById<SearchView>(R.id.search)
): View {
binding = FragmentSearchBinding.inflate(inflater, container, false)
//Configure the search widget (see https://developer.android.com/guide/topics/search/search-dialog#ConfiguringWidget)
// Configure the search widget (see https://developer.android.com/guide/topics/search/search-dialog#ConfiguringWidget)
val searchManager = requireActivity().getSystemService(Context.SEARCH_SERVICE) as SearchManager
search.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().componentName))
search.isSubmitButtonEnabled = true
binding.search.apply {
setSearchableInfo(searchManager.getSearchableInfo(requireActivity().componentName))
isSubmitButtonEnabled = true
}
// Set posts RecyclerView as a grid with 3 columns
recycler = view.findViewById(R.id.discoverList)
recycler = binding.discoverList
recycler.layoutManager = GridLayoutManager(requireContext(), 3)
adapter = DiscoverRecyclerViewAdapter()
recycler.adapter = adapter
val discoverText = view.findViewById<TextView>(R.id.discoverText)
discoverText.setCompoundDrawables(IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_explore).apply {
binding.discoverText.setCompoundDrawables(IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_explore).apply {
sizeDp = 24
paddingDp = 20
color = IconicsColor.colorRes(R.color.colorDrawing)
}, null, null, null)
return view
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -86,30 +87,27 @@ class SearchDiscoverFragment : BaseFragment() {
accessToken = db.userDao().getActiveUser()?.accessToken.orEmpty()
discoverProgressBar = view.findViewById(R.id.discoverProgressBar)
discoverRefreshLayout = view.findViewById(R.id.discoverRefreshLayout)
getDiscover()
discoverRefreshLayout.setOnRefreshListener {
binding.discoverRefreshLayout.setOnRefreshListener {
getDiscover()
}
}
fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){
val motionLayout = view?.findViewById<MotionLayout>(R.id.motionLayout)
if(show){
motionLayout?.transitionToEnd()
} else {
motionLayout?.transitionToStart()
binding.motionLayout.apply {
if(show){
transitionToEnd()
} else {
transitionToStart()
}
}
discoverRefreshLayout.isRefreshing = false
discoverProgressBar.visibility = View.GONE
binding.discoverRefreshLayout.isRefreshing = false
binding.discoverProgressBar.visibility = View.GONE
}
private fun getDiscover() {
lifecycleScope.launchWhenCreated {
try {
val discoverPosts = api.discover("Bearer $accessToken")

View File

@ -4,18 +4,21 @@ import android.content.Intent
import android.os.Bundle
import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ActivityAboutBinding
import com.h.pixeldroid.utils.BaseActivity
import kotlinx.android.synthetic.main.activity_about.*
class AboutActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_about)
val binding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.about_pixeldroid)
aboutVersionNumber.text = BuildConfig.VERSION_NAME
licensesButton.setOnClickListener{
binding.aboutVersionNumber.text = BuildConfig.VERSION_NAME
binding.licensesButton.setOnClickListener{
val intent = Intent(this, LicenseActivity::class.java)
startActivity(intent)
}

View File

@ -2,16 +2,18 @@ package com.h.pixeldroid.settings
import android.os.Bundle
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ActivityLicensesBinding
import com.h.pixeldroid.utils.BaseActivity
import kotlinx.android.synthetic.main.activity_licenses.*
class LicenseActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_licenses)
val binding = ActivityLicensesBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.dependencies_licenses)
webview.loadUrl("file:///android_asset/licenses.html")
binding.webview.loadUrl("file:///android_asset/licenses.html")
}
}

View File

@ -10,7 +10,12 @@ import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.h.pixeldroid.R
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
fun hasInternet(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@ -66,3 +71,25 @@ fun setThemeFromPreferences(preferences: SharedPreferences, resources : Resource
}
}
}
/**
* Delegated property to use in fragments to prevent memory leaks of bindings.
* This makes it unnecessary to set binding to null in onDestroyView.
* The value should be assigned in the Fragment's onCreateView()
*/
fun <T> Fragment.bindingLifecycleAware(): ReadWriteProperty<Fragment, T> =
object : ReadWriteProperty<Fragment, T>, DefaultLifecycleObserver {
private var binding: T? = null
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T = binding!!
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
binding = value
this@bindingLifecycleAware.viewLifecycleOwner.lifecycle.addObserver(this)
}
}

View File

@ -8,19 +8,14 @@ import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.net.Uri
import android.os.Environment
import android.text.Spanned
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.ImageView
import android.widget.TextView
import androidx.core.net.toUri
import com.google.android.material.snackbar.Snackbar
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.databinding.PostFragmentBinding
import com.h.pixeldroid.posts.getDomain
import com.h.pixeldroid.posts.parseHTMLText
import kotlinx.android.synthetic.main.post_fragment.view.*
import java.io.File
import java.io.Serializable
import java.util.*
@ -93,26 +88,26 @@ open class Status(
}
fun setupSensitiveLayout(view: View) {
fun setupSensitiveLayout(binding: PostFragmentBinding) {
// Set dark layout and warning message
view.sensitiveWarning.visibility = VISIBLE
binding.sensitiveWarning.visibility = VISIBLE
val array = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f, 0f)
val censorMatrix = ColorMatrix(array)
view.postPicture.colorFilter = ColorMatrixColorFilter(censorMatrix)
binding.postPicture.colorFilter = ColorMatrixColorFilter(censorMatrix)
fun uncensorPicture(view: View) {
view.sensitiveWarning.visibility = GONE
view.postPicture.clearColorFilter()
fun uncensorPicture(binding: PostFragmentBinding) {
binding.sensitiveWarning.visibility = GONE
binding.postPicture.clearColorFilter()
}
view.findViewById<TextView>(R.id.sensitiveWarning).setOnClickListener {
uncensorPicture(view)
binding.sensitiveWarning.setOnClickListener {
uncensorPicture(binding)
}
view.findViewById<ImageView>(R.id.postPicture).setOnClickListener {
uncensorPicture(view)
binding.postPicture.setOnClickListener {
uncensorPicture(binding)
}
}

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,7v2.99s-1.99,0.01 -2,0L17,7h-3s0.01,-1.99 0,-2h3L17,2h2v3h3v2h-3zM16,11L16,8h-3L13,5L5,5c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-8h-3zM5,19l3,-4 2,3 3,-4 4,5L5,19z"
android:fillColor="@color/colorDrawing"/>
android:fillColor="#FFFFFF"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM8,20L4,20v-4h4v4zM8,14L4,14v-4h4v4zM8,8L4,8L4,4h4v4zM14,20h-4v-4h4v4zM14,14h-4v-4h4v4zM14,8h-4L10,4h4v4zM20,20h-4v-4h4v4zM20,14h-4v-4h4v4zM20,8h-4L16,4h4v4z"
android:fillColor="#000000"/>
</vector>

View File

@ -1,10 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:width="30dp"
android:height="30dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:fillColor="#FFFFFF"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7,19h10L17,4L7,4v15zM2,17h4L6,6L2,6v11zM18,6v11h4L22,6h-4z"
android:fillColor="#000000"/>
</vector>

View File

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context=".postCreation.PostCreationActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/upload_error"
@ -41,29 +43,24 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<com.h.pixeldroid.postCreation.carousel.ImageCarousel
android:id="@+id/carousel"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/uploadProgressBar"
app:layout_constraintTop_toTopOf="parent"/>
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/upload_completed_textview"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="@string/media_upload_completed"
android:textColor="@android:color/holo_green_light"
android:textSize="16sp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/image_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="16dp"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/upload_completed_textview" />
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/buttonConstraints"
@ -79,10 +76,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="@color/colorButtonBg"
android:enabled="false"
android:visibility="gone"
android:enabled="true"
android:text="@string/post"
android:textColor="@color/colorButtonText"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -93,10 +90,11 @@
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
@ -104,6 +102,7 @@
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
@ -133,4 +132,69 @@
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbar3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#40000000"
android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/savePhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/save_to_gallery"
android:tooltipText='@string/save_to_gallery'
android:src="@drawable/download_file_30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/removePhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/delete"
android:tooltipText='@string/delete'
android:src="@drawable/delete_30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/savePhotoButton"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/editPhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/edit"
android:tooltipText='@string/edit'
android:src="@drawable/ic_baseline_edit_30"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/addPhotoButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_photo"
android:tooltipText='@string/add_photo'
android:src="@drawable/add_photo_alternate_white_30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -34,6 +34,7 @@
android:layout_marginStart="8dp"
android:gravity="center"
android:text="@string/default_nposts"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
@ -45,6 +46,7 @@
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/default_nfollowers"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
@ -57,6 +59,7 @@
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="@string/default_nfollowing"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
@ -69,9 +72,10 @@
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
android:text="@string/no_username"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView"/>
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
<TextView

View File

@ -7,10 +7,11 @@
android:focusable="true">
<ImageView
android:id="@+id/addPhotoSquare"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:background="@drawable/add_photo_alternate_black_24dp"
android:background="@drawable/add_photo_alternate_white_30dp"
android:contentDescription="@string/add_photo" />
</com.h.pixeldroid.postCreation.SquareLayout>

View File

@ -7,10 +7,6 @@
android:foreground="?selectableItemBackground"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/galleryImage"
android:layout_width="match_parent"
@ -18,19 +14,4 @@
android:padding="8dp"
android:scaleType="centerCrop"
android:contentDescription="@string/post_image" />
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:background="@drawable/circle_black_24dp"
android:backgroundTint="#7A3E3C3C"
android:foreground="@drawable/ic_baseline_edit_24"
android:foregroundGravity="center"
android:foregroundTint="#FFFFFF"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/click_image_edit" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.h.pixeldroid.postCreation.SquareLayout>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundTint="#FFFFFF">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="5"
tools:listitem="@layout/item_carousel" />
<TextView
android:id="@+id/tv_caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="marquee"
android:gravity="center"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:textAlignment="center"
android:textColor="@color/white"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/indicator"
app:layout_goneMarginBottom="8dp"
tools:text="@tools:sample/lorem[5]" />
<me.relex.circleindicator.CircleIndicator2
android:id="@+id/indicator"
android:layout_width="wrap_content"
android:layout_height="32dp"
app:layout_constraintBottom_toBottomOf="@+id/recyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<FrameLayout
android:id="@+id/previous_button_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/recyclerView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/recyclerView" />
<FrameLayout
android:id="@+id/next_button_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/recyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/recyclerView" />
<ImageButton
android:id="@+id/switchToGridButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="TODO"
android:src="@drawable/grid_on_black_24dp"
android:tint="@color/white"
app:layout_constraintBottom_toBottomOf="@+id/indicator"
app:layout_constraintEnd_toStartOf="@+id/indicator"
app:layout_constraintTop_toTopOf="@+id/indicator" />
<ImageButton
android:id="@+id/switchToCarouselButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="TODO"
android:visibility="gone"
tools:visibility="visible"
android:src="@drawable/view_carousel_black_24dp"
android:tint="@color/white"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@+id/indicator"
app:layout_constraintTop_toTopOf="@+id/indicator" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
tools:ignore="ContentDescription"
tools:src="@tools:sample/backgrounds/scenic" />

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/btn_next"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:backgroundTint="#22000000"
app:cornerRadius="48dp"
app:icon="@drawable/ic_chevron_right_black_24dp"
app:iconGravity="textEnd"
app:iconSize="48dp"
app:iconTint="@color/white"
app:rippleColor="@color/white" />

View File

@ -222,7 +222,7 @@
tools:text="Yesterday" />
<TextView
android:id="@+id/ViewComments"
android:id="@+id/viewComments"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
@ -267,7 +267,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/ViewComments">
app:layout_constraintTop_toBottomOf="@+id/viewComments">
</LinearLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/btn_previous"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:backgroundTint="#22000000"
app:cornerRadius="48dp"
app:icon="@drawable/ic_chevron_left_black_24dp"
app:iconGravity="textStart"
app:iconSize="48dp"
app:iconTint="@color/white"
app:rippleColor="@color/white" />

View File

@ -1,20 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_upload"
android:id="@+id/action_reset"
android:orderInCategory="100"
android:title="CREATE POST"
android:icon="@drawable/ic_file_upload_24dp"
android:title="RESET"
android:icon="@drawable/restore_24dp"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_save"
android:orderInCategory="101"
android:title="SAVE"
android:icon="@drawable/ic_save_24dp"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -19,7 +19,7 @@
<string name="whats_an_instance">ماذا نعني بمثيل الخادم؟</string>
<string name="logout">الخروج</string>
<string name="tab_filters">الفلاتر</string>
<string name="tab_edit">تعديل</string>
<string name="edit">تعديل</string>
<string name="save_to_gallery">احتفظ بها في المعرض…</string>
<string name="image_download_downloading">التنزيل جارٍ…</string>
<string name="image_download_success">تم التنزيل بنجاح</string>

View File

@ -30,7 +30,7 @@
<string name="lbl_contrast">CONTRAST</string>
<string name="lbl_saturation">SATURACIÓ</string>
<string name="tab_filters">FILTRES</string>
<string name="tab_edit">EDITAR</string>
<string name="edit">EDITAR</string>
<string name="capture_button_alt">Captura</string>
<string name="gallery_button_alt">Galeria</string>
<string name="NoCommentsToShow">No hi ha comentaris en aquesta publicació …</string>
@ -101,9 +101,7 @@
<string name="busy_dialog_ok_button">D\'acord, espera per això.</string>
<string name="busy_dialog_text">Processant la imatge, espara abans de continuar!</string>
<string name="nothing_to_see_here">Res per veure aquí!</string>
<string name="OK">D\'ACORD</string>
<string name="delete_dialog">Esborra aquest contingut\?</string>
<string name="cancel">Cancel·lar</string>
<string name="language">Idioma</string>
<string name="poll_notification">%1$s l\'enquesta ha acabat</string>
<string name="about_pixeldroid">Quant a PixelDroid</string>

View File

@ -22,7 +22,7 @@
<string name="logout">Abmelden</string>
<string name="lbl_brightness">Helligkeit</string>
<string name="tab_filters">Filter</string>
<string name="tab_edit">bearbeiten</string>
<string name="edit">bearbeiten</string>
<string name="lbl_contrast">Kontrast</string>
<string name="lbl_saturation">Sättigung</string>
<string name="NoCommentsToShow">Keine Kommentare zu diesem Beitrag…</string>

View File

@ -29,7 +29,7 @@
<string name="lbl_contrast">CONTRASTE</string>
<string name="lbl_saturation">SATURACIÓN</string>
<string name="tab_filters">FILTROS</string>
<string name="tab_edit">EDITAR</string>
<string name="edit">EDITAR</string>
<string name="capture_button_alt">Capturar</string>
<string name="gallery_button_alt">Galería</string>
<string name="NoCommentsToShow">No hay comentarios en esta publicación…</string>

View File

@ -25,7 +25,7 @@
<string name="gallery_button_alt">Galeria</string>
<string name="NoCommentsToShow">Iruzkinik gabeko argitalpena…</string>
<string name="theme_title">Aplikazioaren gaia</string>
<string name="tab_edit">EDITATU</string>
<string name="edit">EDITATU</string>
<string name="switch_camera_button_alt">Aldatu kamera</string>
<string name="capture_button_alt">Atera</string>
<string name="theme_header">Gaia</string>

View File

@ -23,7 +23,7 @@
<string name="lbl_contrast">تضاد</string>
<string name="lbl_saturation">اشباع</string>
<string name="tab_filters">پالایه‌ها</string>
<string name="tab_edit">ویرایش</string>
<string name="edit">ویرایش</string>
<string name="save_to_gallery">ذخیره در نگارخانه…</string>
<string name="image_download_failed">بارگیری شکست خورد، دوباره تلاش کنید</string>
<string name="image_download_downloading">در حال بارگیری…</string>
@ -132,9 +132,7 @@
<string name="issues_contribute">مشکلات را گزارش و یا در توسعه برنامه مشارکت کنید:</string>
<string name="help_translate">برای ترجمه پیکس‌دروید به زبان خود کمک کنید:</string>
<string name="language">زبان</string>
<string name="cancel">لغو</string>
<string name="delete_dialog">این فرسته حذف شود؟</string>
<string name="OK">باشه</string>
<string name="delete">حذف</string>
<string name="post_is_album">این فرسته، یک آلبوم است</string>
<string name="submit_comment">فرستادن نظر</string>

View File

@ -29,7 +29,7 @@
<string name="lbl_contrast">CONTRASTE</string>
<string name="lbl_saturation">SATURATION</string>
<string name="tab_filters">FILTRES</string>
<string name="tab_edit">MODIFIER</string>
<string name="edit">MODIFIER</string>
<string name="capture_button_alt">Prendre une photo</string>
<string name="switch_camera_button_alt">Changer de caméra</string>
<string name="gallery_button_alt">Galerie</string>
@ -112,9 +112,7 @@
<string name="mascot_description">Image montrant un panda rouge (la mascotte de Pixelfed) qui utilise un téléphone</string>
<string name="help_translate">Aidez pour traduire PixelDroid dans votre langue:</string>
<string name="language">Langue</string>
<string name="cancel">Annuler</string>
<string name="delete_dialog">Supprimer cette publication\?</string>
<string name="OK">OK</string>
<string name="delete">Supprimer</string>
<string name="discover">DÉCOUVRIR</string>
<string name="profile_picture">Photo de profil</string>

View File

@ -21,7 +21,7 @@
<string name="lbl_contrast">CONTRASTE</string>
<string name="lbl_saturation">SATURACIÓN</string>
<string name="tab_filters">FILTROS</string>
<string name="tab_edit">EDITAR</string>
<string name="edit">EDITAR</string>
<string name="save_to_gallery">Gardar na Galería…</string>
<string name="image_download_downloading">Descargando…</string>
<string name="image_download_success">Imaxe descargada correctamente</string>
@ -132,8 +132,6 @@
<string name="issues_contribute">Informa de fallos ou colabora coa aplicación:</string>
<string name="help_translate">Axuda a traducir PixelDroid ó teu idioma:</string>
<string name="language">Idioma</string>
<string name="cancel">Cancelar</string>
<string name="delete_dialog">Eliminar esta publicación\?</string>
<string name="OK">OK</string>
<string name="delete">Eliminar</string>
</resources>

View File

@ -37,7 +37,7 @@
<string name="domain_of_your_instance">Dominio della tua istanza</string>
<string name="login_connection_required_once">Devi essere online per poter aggiungere il primo account e utilizzare PixelDroid :(</string>
<string name="invalid_domain">Dominio non valido</string>
<string name="tab_edit">MODIFICA</string>
<string name="edit">MODIFICA</string>
<string name="permission_denied">Permesso negato</string>
<string name="instance_error">Impossibile ottenere informazioni sull\'istanza</string>
<string name="save_image_failed">Impossibile salvare l\'immagine</string>
@ -130,9 +130,7 @@
<string name="something_went_wrong">Qualcosa è andato storto…</string>
<string name="poll_notification">%1$s\'s sondaggio è terminato</string>
<string name="delete">Elimina</string>
<string name="OK">OK</string>
<string name="delete_dialog">Eliminare questo post\?</string>
<string name="cancel">Cancella</string>
<string name="language">Lingua</string>
<string name="help_translate">Aiuta a tradurre PixelDroid nella tua lingua:</string>
<string name="issues_contribute">Segnala problemi o contribuisci all\'applicazione:</string>

View File

@ -35,7 +35,7 @@
<string name="browser_launch_failed">ブラウザが起動できませんでした。インストールされているか確認してください。</string>
<string name="mention_notification">%1$s さんがあなたにメンションしました</string>
<string name="shared_notification">%1$s さんがあなたの投稿を再共有しました</string>
<string name="tab_edit">編集</string>
<string name="edit">編集</string>
<string name="image_download_failed">ダウンロードに失敗しました、もう一度実行してください</string>
<string name="NoCommentsToShow">この投稿にはコメントがありません</string>
<string name="add_account_description">他のPixelfedアカウントを追加</string>

View File

@ -30,7 +30,7 @@
<string name="lbl_contrast">CONTRAST</string>
<string name="lbl_saturation">VERZADIGING</string>
<string name="tab_filters">FILTERS</string>
<string name="tab_edit">BEWERKEN</string>
<string name="edit">BEWERKEN</string>
<string name="capture_button_alt">Vastleggen</string>
<string name="switch_camera_button_alt">Van camera wisselen</string>
<string name="gallery_button_alt">Gallerij</string>
@ -113,9 +113,7 @@
<string name="issues_contribute">Problemen in de app melden of er aan meewerken:</string>
<string name="help_translate">Help mee om PixelDroid naar jouw eigen taal te vertalen:</string>
<string name="language">Taal</string>
<string name="cancel">Annuleren</string>
<string name="delete_dialog">Deze post verwijderen\?</string>
<string name="OK">OK</string>
<string name="delete">Verwijder</string>
<string name="panda_pull_to_refresh_to_try_again">Deze panda ziet er niet gelukkig uit. Trek naar beneden om nog eens te proberen.</string>
<string name="something_went_wrong">Er is iets misgegaan…</string>

View File

@ -33,7 +33,7 @@
<string name="lbl_contrast">KONTRAST</string>
<string name="lbl_saturation">NASYCENIE</string>
<string name="tab_filters">FILTRY</string>
<string name="tab_edit">EDYCJA</string>
<string name="edit">EDYCJA</string>
<string name="normal_filter">Zwykły</string>
<string name="busy_dialog_ok_button">OK, zaczekam.</string>
<string name="crop_result_error">Nie udało się pobrać obrazka po przycięciu</string>

View File

@ -22,7 +22,7 @@
<string name="image_download_downloading">Baixando o arquivo…</string>
<string name="image_download_failed">O download não deu certo, por favor, tente novamente</string>
<string name="save_to_gallery">Salvar na Galeria…</string>
<string name="tab_edit">EDITAR</string>
<string name="edit">EDITAR</string>
<string name="tab_filters">FILTROS</string>
<string name="lbl_saturation">SATURAÇÃO</string>
<string name="lbl_contrast">CONTRASTE</string>

View File

@ -15,7 +15,7 @@
<string name="lbl_contrast">КОНТРАСТ</string>
<string name="lbl_saturation">НАСЫЩЕННОСТЬ</string>
<string name="tab_filters">ФИЛЬТРЫ</string>
<string name="tab_edit">РЕДАКТИРОВАТЬ</string>
<string name="edit">РЕДАКТИРОВАТЬ</string>
<string name="save_to_gallery">Сохранить в Галерею…</string>
<string name="image_download_downloading">Сохранение…</string>
<string name="image_download_success">Изображение успешно сохранено</string>

View File

@ -29,7 +29,7 @@
<string name="lbl_contrast">KONTRAST</string>
<string name="lbl_saturation">FÄRGMÄTTNAD</string>
<string name="tab_filters">FILTER</string>
<string name="tab_edit">REDIGERA</string>
<string name="edit">REDIGERA</string>
<string name="capture_button_alt">Lagra</string>
<string name="switch_camera_button_alt">Byt kamera</string>
<string name="gallery_button_alt">Galleri</string>

View File

@ -22,7 +22,7 @@
<string name="share_picture">Поділитися фотографією…</string>
<string name="logout">Вийти</string>
<string name="NoCommentsToShow">Немає коментарів до цієї публікації…</string>
<string name="tab_edit">Редагувати</string>
<string name="edit">Редагувати</string>
<string name="app_name">PixelDroid</string>
<string name="menu_settings">Налаштування</string>
</resources>

View File

@ -24,7 +24,7 @@
<string name="lbl_contrast">对比度</string>
<string name="lbl_saturation">饱和度</string>
<string name="tab_filters">滤镜</string>
<string name="tab_edit">编辑</string>
<string name="edit">编辑</string>
<string name="save_to_gallery">保存到相册……</string>
<string name="image_download_failed">下载失败,请重试</string>
<string name="image_download_downloading">下载中……</string>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ImageCarousel">
<attr name="showCaption" format="boolean" />
<attr name="captionTextSize" format="dimension" />
<attr name="showIndicator" format="boolean" />
<attr name="showNavigationButtons" format="boolean" />
<attr name="imageScaleType" format="enum">
<enum name="matrix" value="0" />
<enum name="fitXY" value="1" />
<enum name="fitStart" value="2" />
<enum name="fitCenter" value="3" />
<enum name="fitEnd" value="4" />
<enum name="center" value="5" />
<enum name="centerCrop" value="6" />
<enum name="centerInside" value="7" />
</attr>
<attr name="imagePlaceholder" format="reference|color" />
<attr name="itemLayout" format="reference" />
<attr name="imageViewId" format="reference" />
<attr name="showLayoutSwitchButton" format="boolean" />
<attr name="layoutCarousel" format="boolean" />
</declare-styleable>
</resources>

View File

@ -14,5 +14,6 @@
<color name="filterLabelSelected">#221F20</color>
<color name="colorPrimaryError">#FF0000</color>
<color name="colorText">#FFFFFF</color>
<color name="white">#FFFFFF</color>
<color name="colorDrawing">#000000</color>
</resources>

View File

@ -56,7 +56,7 @@
<string name="lbl_contrast">CONTRAST</string>
<string name="lbl_saturation">SATURATION</string>
<string name="tab_filters">FILTERS</string>
<string name="tab_edit">EDIT</string>
<string name="edit">Edit</string>
<string name="filter_thumbnail">Thumbnail of filter</string>
<string name="normal_filter">Normal</string>
<string name="busy_dialog_text">Still processing image, wait for that to finish first!</string>
@ -149,12 +149,12 @@
<string name="something_went_wrong">Something went wrong…</string>
<string name="panda_pull_to_refresh_to_try_again">This panda is not happy. Pull to refresh to try again.</string>
<string name="delete">Delete</string>
<string name="OK">OK</string>
<string name="delete_dialog">Delete this post?</string>
<string name="cancel">Cancel</string>
<string name="language">Language</string>
<string name="help_translate">Help translate PixelDroid to your language:</string>
<string name="issues_contribute">Report issues or contribute to the application:</string>
<string name="mascot_description">Image showing a red panda, Pixelfed\'s mascot, using a phone</string>
<string name="save_before_returning">Save your edits?</string>
<string name="no_cancel_edit">No, cancel edit</string>
</resources>

View File

@ -27,6 +27,6 @@
app:icon="@drawable/info_black_24dp">
<intent
android:targetPackage="com.h.pixeldroid"
android:targetClass="com.h.pixeldroid.AboutActivity"/>
android:targetClass="com.h.pixeldroid.settings.AboutActivity"/>
</Preference>
</PreferenceScreen>