Merge branch 'crash_fixes' into 'master'

Crash fixes, various small improvements

Closes #255 and #223

See merge request pixeldroid/PixelDroid!254
This commit is contained in:
Matthieu 2020-11-02 22:45:53 +01:00
commit ee06e0229c
43 changed files with 728 additions and 451 deletions

View File

@ -7,10 +7,7 @@ Free (as in freedom) Android client for Pixelfed, the federated image sharing pl
<a href=https://apt.izzysoft.de/fdroid/index/apk/com.h.pixeldroid><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="170"></a>
## Compiling the code yourself
If you want to try out PixelDroid on your own device, you can try to compile the source code yourself. To do that you will need to install [Android Studio](https://developer.android.com/studio/).
- Open the ___gradle___ project inside of Android Studio. Then you should plug your Android device into your computer (make sure that your device is in [developer mode](https://developer.android.com/studio/debug/dev-options)) and select ___share files___ on it.
If you want to try out PixelDroid on your own device, you can compile the source code yourself. To do that you can install [Android Studio](https://developer.android.com/studio/).
- You should see that Android studio has detected your device and its name should appear next to a small play button on the top right corner of Android Studio. If that is the case, then you can click said play button and, after Android studio will have built the project, you'll be able to use PixelDroid on your device!
At this point PixelDroid will be installed on your phone, so it won't have to be plugged in anymore!
## Art attribution
Various works have been used from the pixelfed branding repository ( https://github.com/pixelfed/brand-assets ). In addition, a drawing of a red panda is used for some error messages ( https://thenounproject.com/search/?q=red+panda&i=2877785 )

View File

@ -66,15 +66,15 @@ dependencies {
*/
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.navigation:navigation-fragment:2.3.0'
implementation 'androidx.navigation:navigation-ui:2.3.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
implementation "androidx.browser:browser:1.2.0"
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
@ -83,14 +83,14 @@ dependencies {
implementation 'androidx.gridlayout:gridlayout:1.0.0'
// Use the most recent version of CameraX
def camerax_version = '1.0.0-beta08'
def camerax_version = '1.0.0-beta11'
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class
implementation 'androidx.camera:camera-view:1.0.0-alpha15'
implementation 'androidx.camera:camera-view:1.0.0-alpha18'
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
@ -106,13 +106,13 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1'
//Dagger (dependency injection)
implementation 'com.google.dagger:dagger-android:2.28.3'
implementation 'com.google.dagger:dagger-android-support:2.28.3'
implementation 'com.google.dagger:dagger-android:2.29.1'
implementation 'com.google.dagger:dagger-android-support:2.29.1'
// if you use the support libraries
kapt 'com.google.dagger:dagger-android-processor:2.28.3'
kapt 'com.google.dagger:dagger-compiler:2.28.3'
kapt 'com.google.dagger:dagger-android-processor:2.29.1'
kapt 'com.google.dagger:dagger-compiler:2.29.1'
implementation 'com.squareup.okhttp3:okhttp:4.8.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
@ -160,9 +160,9 @@ dependencies {
debugImplementation "androidx.fragment:fragment-testing:1.2.5"
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.27.1'
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.27.2'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
testImplementation 'junit:junit:4.13'
testImplementation 'junit:junit:4.13.1'
testImplementation "androidx.room:room-testing:$room_version"
@ -175,7 +175,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
androidTestImplementation('com.squareup.okhttp3:mockwebserver:4.8.0')
androidTestImplementation('com.squareup.okhttp3:mockwebserver:4.9.0')
}

View File

@ -19,9 +19,6 @@ class FollowsActivity : AppCompatActivity() {
private var followsFragment = AccountListFragment()
@Inject
lateinit var db: AppDatabase
@Inject
lateinit var apiHolder: PixelfedAPIHolder
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -35,28 +32,10 @@ class FollowsActivity : AppCompatActivity() {
val followers = intent.getSerializableExtra(FOLLOWERS_TAG) as Boolean
if(account == null) {
val user = db.userDao().getActiveUser()
val accessToken = user?.accessToken.orEmpty()
val pixelfedAPI = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
pixelfedAPI.verifyCredentials("Bearer $accessToken").enqueue(object :
Callback<Account> {
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("Cannot get account id", t.toString())
}
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if(response.code() == 200) {
val id = response.body()!!.id
val displayName = response.body()!!.display_name
startFragment(id, displayName, followers)
}
}
})
val user = db.userDao().getActiveUser()!!
startFragment(user.user_id, user.display_name, followers)
} else {
startFragment(account.id, account.display_name, followers)
startFragment(account.id!!, account.getDisplayName(), followers)
}
}

View File

@ -190,7 +190,7 @@ class MainActivity : AppCompatActivity() {
if (response.body() != null && response.isSuccessful) {
val account = response.body() as Account
DBUtils.addUser(db, account, domain, accessToken = accessToken)
fillDrawerAccountInfo(account.id)
fillDrawerAccountInfo(account.id!!)
}
}
@ -248,17 +248,16 @@ class MainActivity : AppCompatActivity() {
iconUrl = user.avatar_static
isNameShown = true
identifier = user.user_id.toLong()
descriptionText = "${user.username}@${user.instance_uri.removePrefix("https://")}"
descriptionText = "@${user.username}@${user.instance_uri.removePrefix("https://")}"
}
}.toMutableList()
// reuse the already existing "add account" item
for (profile in header.profiles.orEmpty()) {
if (profile.identifier == ADD_ACCOUNT_IDENTIFIER) {
profiles.add(profile)
break
}
}
header.profiles.orEmpty()
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
.take(1)
.map { profiles.add(it) }
header.clear()
header.profiles = profiles
header.setActiveProfile(account.toLong())

View File

@ -129,7 +129,7 @@ class PhotoEditActivity : AppCompatActivity(), FilterListFragmentListener, EditI
//<editor-fold desc="ON LAUNCH">
private fun loadImage() {
originalImage = MediaStore.Images.Media.getBitmap(contentResolver, imageUri)
compressedImage = resizeImage(originalImage!!.copy(BITMAP_CONFIG, true))
compressedImage = resizeImage(originalImage!!)
compressedOriginalImage = compressedImage!!.copy(BITMAP_CONFIG, true)
filteredImage = compressedImage!!.copy(BITMAP_CONFIG, true)
Glide.with(this).load(compressedImage).into(image_preview)

View File

@ -86,7 +86,7 @@ class PostActivity : AppCompatActivity() {
}
private fun initializeFragment(arguments: Bundle, status: Status?){
supportActionBar?.title = getString(R.string.post_title).format(status!!.account?.display_name)
supportActionBar?.title = getString(R.string.post_title).format(status!!.account?.getDisplayName())
arguments.putSerializable(POST_TAG, status)
postFragment.arguments = arguments
supportFragmentManager.beginTransaction()

View File

@ -6,23 +6,25 @@ import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import android.widget.*
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.h.pixeldroid.adapters.ProfilePostsRecyclerViewAdapter
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.di.PixelfedAPIHolder
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Relationship
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.HtmlUtils.Companion.parseHTMLText
import com.h.pixeldroid.utils.ImageConverter
import kotlinx.android.synthetic.main.fragment_search.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -32,9 +34,11 @@ class ProfileActivity : AppCompatActivity() {
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 account: Account? = null
private var user: UserDatabaseEntity? = null
@Inject
lateinit var db: AppDatabase
@ -49,7 +53,7 @@ class ProfileActivity : AppCompatActivity() {
(this.application as Pixeldroid).getAppComponent().inject(this)
val user = db.userDao().getActiveUser()
user = db.userDao().getActiveUser()
domain = user?.instance_uri.orEmpty()
pixelfedAPI = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
@ -61,7 +65,16 @@ class ProfileActivity : AppCompatActivity() {
adapter = ProfilePostsRecyclerViewAdapter()
recycler.adapter = adapter
setContent()
// Set profile according to given account
val account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account?
setContent(account)
refreshLayout = findViewById(R.id.profileRefreshLayout)
refreshLayout.setOnRefreshListener {
getAndSetAccount(account?.id ?: user!!.user_id)
}
}
override fun onSupportNavigateUp(): Boolean {
@ -69,95 +82,128 @@ class ProfileActivity : AppCompatActivity() {
return true
}
private fun setContent() {
// Set profile according to given account
account = intent.getSerializableExtra(Account.ACCOUNT_TAG) as Account?
account?.let {
setViews()
activateFollow()
setPosts()
} ?: run {
private fun setContent(account: Account?) {
if(account != null){
setViews(account)
setPosts(account)
} else {
pixelfedAPI.verifyCredentials("Bearer $accessToken")
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.code() == 200) {
account = response.body()!!
val myAccount = response.body()!!
setViews()
setViews(myAccount)
// Populate profile page with user's posts
setPosts()
setPosts(myAccount)
} else {
showError()
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("ProfileActivity:", t.toString())
showError()
}
})
// Edit button redirects to Pixelfed's "edit account" page
val editButton = findViewById<Button>(R.id.editButton)
editButton.visibility = View.VISIBLE
editButton.setOnClickListener{ onClickEditButton() }
}
//if we aren't viewing our own account, activate follow button
if(account != null && account.id != user?.user_id) activateFollow(account)
//if we *are* viewing our own account, activate the edit button
else activateEditButton()
// On click open followers list
findViewById<TextView>(R.id.nbFollowersTextView).setOnClickListener{ onClickFollowers() }
findViewById<TextView>(R.id.nbFollowersTextView).setOnClickListener{ onClickFollowers(account) }
// On click open followers list
findViewById<TextView>(R.id.nbFollowingTextView).setOnClickListener{ onClickFollowing() }
findViewById<TextView>(R.id.nbFollowingTextView).setOnClickListener{ onClickFollowing(account) }
}
private fun getAndSetAccount(id: String){
pixelfedAPI.getAccount("Bearer $accessToken", id)
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
if (response.code() == 200) {
val account = response.body()!!
setContent(account)
} else {
showError()
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("ProfileActivity:", t.toString())
showError()
}
})
}
private fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){
val motionLayout = findViewById<MotionLayout>(R.id.motionLayout)
if(show){
motionLayout?.transitionToEnd()
} else {
findViewById<ProgressBar>(R.id.profileProgressBar).visibility = View.GONE
motionLayout?.transitionToStart()
}
refreshLayout.isRefreshing = false
}
/**
* Populate myProfile page with user's data
* Populate profile page with user's data
*/
private fun setViews() {
private fun setViews(account: Account) {
val profilePicture = findViewById<ImageView>(R.id.profilePictureImageView)
ImageConverter.setRoundImageFromURL(
View(applicationContext),
account!!.avatar,
account.avatar,
profilePicture
)
val description = findViewById<TextView>(R.id.descriptionTextView)
description.text = parseHTMLText(
account!!.note, emptyList(), pixelfedAPI,
account.note ?: "", emptyList(), pixelfedAPI,
applicationContext, "Bearer $accessToken"
)
val accountName = findViewById<TextView>(R.id.accountNameTextView)
accountName.text = account!!.display_name
accountName.text = account.getDisplayName()
supportActionBar?.title = account!!.display_name
if(account!!.display_name != account!!.acct){
supportActionBar?.subtitle = "@${account!!.acct}"
val displayName = account.getDisplayName()
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)
.format(account!!.statuses_count.toString())
.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)
.format(account!!.followers_count.toString())
.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)
.format(account!!.following_count.toString())
.format(account.following_count.toString())
nbFollowing.setTypeface(null, Typeface.BOLD)
}
/**
* Populate profile page with user's posts
*/
private fun setPosts() {
pixelfedAPI.accountPosts("Bearer $accessToken", account_id = account!!.id)
private fun setPosts(account: Account) {
pixelfedAPI.accountPosts("Bearer $accessToken", account_id = account.id)
.enqueue(object : Callback<List<Status>> {
override fun onFailure(call: Call<List<Status>>, t: Throwable) {
showError()
Log.e("ProfileActivity.Posts:", t.toString())
}
@ -168,6 +214,9 @@ class ProfileActivity : AppCompatActivity() {
if (response.code() == 200) {
val statuses = response.body()!!
adapter.addPosts(statuses)
showError(show = false)
} else {
showError()
}
}
})
@ -184,15 +233,15 @@ class ProfileActivity : AppCompatActivity() {
}
}
private fun onClickFollowers() {
private fun onClickFollowers(account: Account?) {
val intent = Intent(this, FollowsActivity::class.java)
intent.putExtra(Account.FOLLOWERS_TAG, true)
intent.putExtra(Account.ACCOUNT_ID_TAG, account?.id)
intent.putExtra(Account.ACCOUNT_TAG, account)
ContextCompat.startActivity(this, intent, null)
}
private fun onClickFollowing() {
private fun onClickFollowing(account: Account?) {
val intent = Intent(this, FollowsActivity::class.java)
intent.putExtra(Account.FOLLOWERS_TAG, false)
intent.putExtra(Account.ACCOUNT_TAG, account)
@ -200,12 +249,19 @@ class ProfileActivity : AppCompatActivity() {
ContextCompat.startActivity(this, intent, null)
}
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() }
}
/**
* Set up follow button
*/
private fun activateFollow() {
private fun activateFollow(account: Account) {
// Get relationship between the two users (credential and this) and set followButton accordingly
pixelfedAPI.checkRelationships("Bearer $accessToken", listOf(account!!.id))
pixelfedAPI.checkRelationships("Bearer $accessToken", listOf(account.id.orEmpty()))
.enqueue(object : Callback<List<Relationship>> {
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
@ -225,9 +281,9 @@ class ProfileActivity : AppCompatActivity() {
val followButton = findViewById<Button>(R.id.followButton)
if (response.body()!![0].following) {
setOnClickUnfollow()
setOnClickUnfollow(account)
} else {
setOnClickFollow()
setOnClickFollow(account)
}
followButton.visibility = View.VISIBLE
}
@ -241,13 +297,13 @@ class ProfileActivity : AppCompatActivity() {
})
}
private fun setOnClickFollow() {
private fun setOnClickFollow(account: Account) {
val followButton = findViewById<Button>(R.id.followButton)
followButton.setText(R.string.follow)
followButton.setOnClickListener {
pixelfedAPI.follow(account!!.id, "Bearer $accessToken")
pixelfedAPI.follow(account.id.orEmpty(), "Bearer $accessToken")
.enqueue(object : Callback<Relationship> {
override fun onFailure(call: Call<Relationship>, t: Throwable) {
@ -263,7 +319,7 @@ class ProfileActivity : AppCompatActivity() {
response: Response<Relationship>
) {
if (response.code() == 200) {
setOnClickUnfollow()
setOnClickUnfollow(account)
} else if (response.code() == 403) {
Toast.makeText(
applicationContext, getString(R.string.action_not_allowed),
@ -275,13 +331,13 @@ class ProfileActivity : AppCompatActivity() {
}
}
private fun setOnClickUnfollow() {
private fun setOnClickUnfollow(account: Account) {
val followButton = findViewById<Button>(R.id.followButton)
followButton.setText(R.string.unfollow)
followButton.setOnClickListener {
pixelfedAPI.unfollow(account!!.id, "Bearer $accessToken")
pixelfedAPI.unfollow(account.id.orEmpty(), "Bearer $accessToken")
.enqueue(object : Callback<Relationship> {
override fun onFailure(call: Call<Relationship>, t: Throwable) {
@ -297,7 +353,7 @@ class ProfileActivity : AppCompatActivity() {
response: Response<Relationship>
) {
if (response.code() == 200) {
setOnClickFollow()
setOnClickFollow(account)
} else if (response.code() == 401) {
Toast.makeText(
applicationContext, getString(R.string.access_token_invalid),

View File

@ -16,22 +16,22 @@ import com.h.pixeldroid.utils.ImageConverter.Companion.setSquareImageFromURL
/**
* [RecyclerView.Adapter] that can display a list of [Status]s
*/
class ProfilePostsRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostsRecyclerViewAdapter.ViewHolder>() {
class ProfilePostsRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostViewHolder>() {
private val posts: ArrayList<Status> = ArrayList()
fun addPosts(newPosts : List<Status>) {
val size = posts.size
posts.clear()
posts.addAll(newPosts)
notifyItemRangeInserted(size, newPosts.size)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfilePostViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_profile_posts, parent, false)
return ViewHolder(view)
return ProfilePostViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
override fun onBindViewHolder(holder: ProfilePostViewHolder, position: Int) {
val post = posts[position]
if (post.sensitive!!)
@ -39,6 +39,12 @@ class ProfilePostsRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostsRecycler
else
setSquareImageFromURL(holder.postView, post.getPostPreviewURL(), holder.postPreview)
if(post.media_attachments?.size ?: 0 > 1){
holder.albumIcon.visibility = View.VISIBLE
} else {
holder.albumIcon.visibility = View.GONE
}
holder.postPreview.setOnClickListener {
val intent = Intent(holder.postPreview.context, PostActivity::class.java)
intent.putExtra(Status.POST_TAG, post)
@ -47,8 +53,9 @@ class ProfilePostsRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostsRecycler
}
override fun getItemCount(): Int = posts.size
inner class ViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) {
val postPreview: ImageView = postView.findViewById(R.id.postPreview)
}
}
class ProfilePostViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) {
val postPreview: ImageView = postView.findViewById(R.id.postPreview)
val albumIcon: ImageView = postView.findViewById(R.id.albumIcon)
}

View File

@ -184,7 +184,7 @@ interface PixelfedAPI {
@Query("q") q: String,
@Query("resolve") resolve: Boolean? = null,
@Query("limit") limit: String? = null,
@Query("offset") offset: Int? = null,
@Query("offset") offset: String? = null,
@Query("following") following: Boolean? = null
): Call<Results>
@ -229,7 +229,8 @@ interface PixelfedAPI {
@Header("Authorization") authorization: String,
@Query("max_id") max_id: String? = null,
@Query("since_id") since_id: String? = null,
@Query("limit") limit: Number? = null
@Query("limit") limit: Number? = null,
@Query("page") page: String? = null
) : Call<List<Account>>
@GET("/api/v1/accounts/{id}/following")
@ -238,7 +239,8 @@ interface PixelfedAPI {
@Header("Authorization") authorization: String,
@Query("max_id") max_id: String? = null,
@Query("since_id") since_id: String? = null,
@Query("limit") limit: Number? = 40
@Query("limit") limit: Number? = 40,
@Query("page") page: String? = null
) : Call<List<Account>>
@GET("/api/v1/accounts/{id}")

View File

@ -3,6 +3,7 @@ package com.h.pixeldroid.fragments
import android.Manifest
import android.app.Activity
import android.content.ClipData
import android.content.ContentUris
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
@ -31,7 +32,6 @@ import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.h.pixeldroid.PostCreationActivity
import com.h.pixeldroid.CameraActivity
import com.h.pixeldroid.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -107,8 +107,6 @@ class CameraFragment : Fragment() {
// Shut down our background executor
cameraExecutor.shutdown()
// Unregister the broadcast receivers and listeners
}
override fun onCreateView(
@ -121,7 +119,7 @@ class CameraFragment : Fragment() {
return inflater.inflate(R.layout.fragment_camera, container, false)
}
private fun setGalleryThumbnail(uri: String) {
private fun setGalleryThumbnail(uri: Uri) {
// Reference of the view that holds the gallery thumbnail
val thumbnail = container.findViewById<ImageButton>(R.id.photo_view_button)
@ -220,7 +218,7 @@ class CameraFragment : Fragment() {
)
// Attach the viewfinder's surface provider to preview use case
preview?.setSurfaceProvider(viewFinder.createSurfaceProvider())
preview?.setSurfaceProvider(viewFinder.surfaceProvider)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
@ -263,19 +261,22 @@ class CameraFragment : Fragment() {
// Find the last picture
val projection = arrayOf(
MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.MIME_TYPE
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) MediaStore.Images.ImageColumns.DATE_TAKEN
else MediaStore.Images.ImageColumns.DATE_MODIFIED,
)
val cursor = requireContext().contentResolver
.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null,
null, MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC"
null,
(if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) MediaStore.Images.ImageColumns.DATE_TAKEN
else MediaStore.Images.ImageColumns.DATE_MODIFIED) + " DESC"
)
if (cursor != null && cursor.moveToFirst()) {
val uri = Uri.parse(cursor.getString(1)).path ?: ""
setGalleryThumbnail(uri)
val url = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
cursor.getLong(0)
)
setGalleryThumbnail(url)
cursor.close()
}
}

View File

@ -1,41 +0,0 @@
package com.h.pixeldroid.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.h.pixeldroid.R
import com.h.pixeldroid.adapters.ProfilePostsRecyclerViewAdapter
/**
* A fragment representing a list of statuses of a profile.
*/
class ProfilePostsFragment : Fragment() {
private var columnCount = 3
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_profile_posts_list, container, false)
// Set the adapter
if (view is RecyclerView) {
with(view) {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
adapter = ProfilePostsRecyclerViewAdapter()
}
}
return view
}
}

View File

@ -9,28 +9,28 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat.getSystemService
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.textview.MaterialTextView
import com.h.pixeldroid.Pixeldroid
import com.h.pixeldroid.PostActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.SearchActivity
import com.h.pixeldroid.adapters.ProfilePostViewHolder
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.di.PixelfedAPIHolder
import com.h.pixeldroid.objects.DiscoverPost
import com.h.pixeldroid.objects.DiscoverPosts
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.DBUtils
import com.h.pixeldroid.utils.ImageConverter
import com.mikepenz.iconics.IconicsColor
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.padding
import com.mikepenz.iconics.utils.color
import com.mikepenz.iconics.utils.paddingDp
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.android.synthetic.main.fragment_search.*
@ -50,6 +50,7 @@ class SearchDiscoverFragment : Fragment() {
private lateinit var accessToken: String
private lateinit var discoverProgressBar: ProgressBar
private lateinit var discoverRefreshLayout: SwipeRefreshLayout
@Inject
lateinit var db: AppDatabase
@ -85,6 +86,7 @@ class SearchDiscoverFragment : Fragment() {
discoverText.setCompoundDrawables(IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_explore).apply {
sizeDp = 24
paddingDp = 20
color = IconicsColor.colorRes(R.color.colorDrawing)
}, null, null, null)
return view
@ -107,12 +109,25 @@ class SearchDiscoverFragment : Fragment() {
}
}
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()
}
discoverRefreshLayout.isRefreshing = false
discoverProgressBar.visibility = View.GONE
}
private fun getDiscover() {
api.discover("Bearer $accessToken")
.enqueue(object : Callback<DiscoverPosts> {
override fun onFailure(call: Call<DiscoverPosts>, t: Throwable) {
showError()
Log.e("SearchDiscoverFragment:", t.toString())
}
@ -120,16 +135,19 @@ class SearchDiscoverFragment : Fragment() {
if(response.code() == 200) {
val discoverPosts = response.body()!!
adapter.addPosts(discoverPosts.posts)
discoverProgressBar.visibility = View.GONE
discoverRefreshLayout.isRefreshing = false
showError(show = false)
}
else {
showError()
}
}
})
}
/**
* [RecyclerView.Adapter] that can display a list of [DiscoverPost]s
*/
class DiscoverRecyclerViewAdapter: RecyclerView.Adapter<DiscoverRecyclerViewAdapter.ViewHolder>() {
class DiscoverRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostViewHolder>() {
private val posts: ArrayList<DiscoverPost> = ArrayList()
fun addPosts(newPosts : List<DiscoverPost>) {
@ -138,14 +156,19 @@ class SearchDiscoverFragment : Fragment() {
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfilePostViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fragment_profile_posts, parent, false)
return ViewHolder(view)
return ProfilePostViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
override fun onBindViewHolder(holder: ProfilePostViewHolder, position: Int) {
val post = posts[position]
if(post.type?.contains("album") == true) {
holder.albumIcon.visibility = View.VISIBLE
} else {
holder.albumIcon.visibility = View.GONE
}
ImageConverter.setSquareImageFromURL(holder.postView, post.thumb, holder.postPreview)
holder.postPreview.setOnClickListener {
val intent = Intent(holder.postView.context, PostActivity::class.java)
@ -155,9 +178,5 @@ class SearchDiscoverFragment : Fragment() {
}
override fun getItemCount(): Int = posts.size
inner class ViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) {
val postPreview: ImageView = postView.findViewById(R.id.postPreview)
}
}
}

View File

@ -9,7 +9,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.RecyclerView
import androidx.lifecycle.Observer
@ -34,7 +33,9 @@ open class AccountListFragment : FeedFragment() {
lateinit var profilePicRequest: RequestBuilder<Drawable>
protected lateinit var adapter : FeedsRecyclerViewAdapter<Account, AccountsRecyclerViewAdapter.ViewHolder>
lateinit var factory: FeedDataSourceFactory<String, Account>
lateinit var content: LiveData<PagedList<Account>>
private var currentPage = 1
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@ -62,7 +63,7 @@ open class AccountListFragment : FeedFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val content = makeContent()
content = makeContent()
content.observe(viewLifecycleOwner,
Observer { c ->
@ -72,6 +73,9 @@ open class AccountListFragment : FeedFragment() {
})
swipeRefreshLayout.setOnRefreshListener {
showError(show = false)
currentPage = 1
//by invalidating data, loadInitial will be called again
factory.liveData.value!!.invalidate()
}
@ -96,7 +100,7 @@ open class AccountListFragment : FeedFragment() {
//We use the id as the key
override fun getKey(item: Account): String {
return item.id
return currentPage.toString()
}
override fun makeInitialCall(requestedLoadSize: Int): Call<List<Account>> {
@ -114,15 +118,18 @@ open class AccountListFragment : FeedFragment() {
}
override fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Account>> {
// Pixelfed and Mastodon don't implement this in the same fashion. Pixelfed uses
// Laravel's paging mechanism, while Mastodon uses the Link header for pagination.
// No need to know which is which, they should ignore the non-relevant argument
return if (following) {
pixelfedAPI.followers(
id, "Bearer $accessToken",
since_id = key, limit = requestedLoadSize
limit = requestedLoadSize, page = key, max_id = key
)
} else {
pixelfedAPI.following(
id, "Bearer $accessToken",
since_id = key, limit = requestedLoadSize
limit = requestedLoadSize, page = key, max_id = key
)
}
}
@ -133,16 +140,30 @@ open class AccountListFragment : FeedFragment() {
override fun onResponse(call: Call<List<Account>>, response: Response<List<Account>>) {
if (response.isSuccessful && response.body() != null) {
val data = response.body()!!
if(response.headers()["Link"] != null){
//Header is of the form:
// Link: <https://mastodon.social/api/v1/accounts/1/followers?limit=2&max_id=7628164>; rel="next", <https://mastodon.social/api/v1/accounts/1/followers?limit=2&since_id=7628165>; rel="prev"
// So we want the first max_id value. In case there are arguments after
// the max_id in the URL, we make sure to stop at the first '?'
currentPage = response.headers()["Link"]
.orEmpty()
.substringAfter("max_id=")
.substringBefore('?')
.substringBefore('>')
.toIntOrNull() ?: 0
} else {
currentPage++
}
callback.onResult(data)
} else{
Toast.makeText(context, getString(R.string.loading_toast), Toast.LENGTH_SHORT).show()
showError()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<List<Account>>, t: Throwable) {
Toast.makeText(context, getString(R.string.feed_failed), Toast.LENGTH_SHORT).show()
showError(errorText = R.string.feed_failed)
Log.e("AccountListFragment", t.toString())
}
})

View File

@ -1,11 +1,16 @@
package com.h.pixeldroid.fragments.feeds
package com.h.pixeldroid.fragments.feeds
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.MutableLiveData
import androidx.paging.DataSource
@ -22,7 +27,9 @@ import com.h.pixeldroid.db.AppDatabase
import com.h.pixeldroid.db.UserDatabaseEntity
import com.h.pixeldroid.di.PixelfedAPIHolder
import com.h.pixeldroid.objects.FeedContent
import kotlinx.android.synthetic.main.fragment_feed.*
import kotlinx.android.synthetic.main.fragment_feed.view.*
import org.w3c.dom.Text
import retrofit2.Call
import javax.inject.Inject
@ -65,6 +72,20 @@ open class FeedFragment: Fragment() {
return view
}
fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){
val errorLayout = view?.findViewById<ConstraintLayout>(R.id.errorLayout)
val progressBar = view?.findViewById<ProgressBar>(R.id.progressBar)
if(show){
view?.findViewById<TextView>(R.id.error_text)?.setText(errorText)
errorLayout?.visibility = VISIBLE
progressBar?.visibility = GONE
} else {
errorLayout?.visibility = GONE
progressBar?.visibility = VISIBLE
}
}
open inner class FeedDataSourceFactory<ObjectId, APIObject: FeedContent>(
private val dataSource: FeedDataSource<ObjectId, APIObject>
): DataSource.Factory<ObjectId, APIObject>() {

View File

@ -10,7 +10,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.paging.LivePagedListBuilder
@ -86,6 +85,8 @@ class NotificationsFragment : FeedFragment() {
})
swipeRefreshLayout.setOnRefreshListener {
showError(show = false)
//by invalidating data, loadInitial will be called again
factory.liveData.value!!.invalidate()
}
@ -125,15 +126,15 @@ class NotificationsFragment : FeedFragment() {
if (response.isSuccessful && response.body() != null) {
val data = response.body()!!
callback.onResult(data)
} else{
Toast.makeText(context, getString(R.string.loading_toast), Toast.LENGTH_SHORT).show()
} else {
showError()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<List<Notification>>, t: Throwable) {
Toast.makeText(context, getString(R.string.feed_failed), Toast.LENGTH_SHORT).show()
showError(errorText = R.string.feed_failed)
Log.e("NotificationsFragment", t.toString())
}
})
@ -155,7 +156,7 @@ class NotificationsFragment : FeedFragment() {
}
}
private fun openPostFromNotifcation(notification: Notification) : Intent {
private fun openPostFromNotification(notification: Notification) : Intent {
val intent = Intent(context, PostActivity::class.java)
intent.putExtra(Status.POST_TAG, notification.status)
return intent
@ -164,11 +165,9 @@ class NotificationsFragment : FeedFragment() {
private fun openActivity(notification: Notification){
val intent: Intent
when (notification.type){
Notification.NotificationType.mention, Notification.NotificationType.favourite-> {
intent = openPostFromNotifcation(notification)
}
Notification.NotificationType.reblog-> {
intent = openPostFromNotifcation(notification)
Notification.NotificationType.mention, Notification.NotificationType.favourite,
Notification.NotificationType.poll, Notification.NotificationType.reblog -> {
intent = openPostFromNotification(notification)
}
Notification.NotificationType.follow -> {
intent = Intent(context, ProfileActivity::class.java)
@ -197,7 +196,7 @@ class NotificationsFragment : FeedFragment() {
holder.photoThumbnail.visibility = View.GONE
}
setNotificationType(notification.type, notification.account.username, holder.notificationType)
setNotificationType(notification.type, notification.account.username!!, holder.notificationType)
setTextViewFromISO8601(notification.created_at, holder.notificationTime, false, context)
//Convert HTML to clickable text
@ -236,6 +235,9 @@ class NotificationsFragment : FeedFragment() {
Notification.NotificationType.favourite -> {
setNotificationTypeTextView(context, R.string.liked_notification, R.drawable.ic_like_full)
}
Notification.NotificationType.poll -> {
setNotificationTypeTextView(context, R.string.poll_notification, R.drawable.poll)
}
}
textView.text = format.format(username)
textView.setCompoundDrawablesWithIntrinsicBounds(

View File

@ -53,15 +53,15 @@ class HomeTimelineFragment: PostsFeedFragment() {
val notifications = response.body()!!
callback.onResult(notifications)
DBUtils.storePosts(db, notifications, user!!)
} else{
Toast.makeText(context, getString(R.string.loading_toast), Toast.LENGTH_SHORT).show()
} else {
showError()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<List<Status>>, t: Throwable) {
Toast.makeText(context, getString(R.string.feed_failed), Toast.LENGTH_SHORT).show()
showError(errorText = R.string.feed_failed)
Log.e("PostsFeedFragment", t.toString())
}
})

View File

@ -68,6 +68,8 @@ abstract class PostsFeedFragment : FeedFragment() {
})
swipeRefreshLayout.setOnRefreshListener {
showError(show = false)
//by invalidating data, loadInitial will be called again
factory.liveData.value!!.invalidate()
}

View File

@ -34,14 +34,14 @@ class PublicTimelineFragment: PostsFeedFragment() {
val notifications = response.body()!!
callback.onResult(notifications)
} else{
Toast.makeText(context, getString(R.string.loading_toast), Toast.LENGTH_SHORT).show()
showError()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<List<Status>>, t: Throwable) {
Toast.makeText(context, getString(R.string.feed_failed), Toast.LENGTH_SHORT).show()
showError(errorText = R.string.feed_failed)
Log.e("PublicTimelineFragment", t.toString())
}
})

View File

@ -42,18 +42,18 @@ class SearchAccountFragment: AccountListFragment(){
}
override fun getKey(item: Account): String {
return item.id
return content.value?.loadedCount.toString()
}
private fun searchMakeInitialCall(requestedLoadSize: Int): Call<Results> {
return pixelfedAPI
.search("Bearer $accessToken",
limit="$requestedLoadSize", q=query,
limit="$requestedLoadSize", q = query,
type = Results.SearchType.accounts)
}
private fun searchMakeAfterCall(requestedLoadSize: Int, key: String): Call<Results> {
return pixelfedAPI
.search("Bearer $accessToken", max_id=key,
.search("Bearer $accessToken", offset = key,
limit="$requestedLoadSize", q = query,
type = Results.SearchType.accounts)
}
@ -75,16 +75,15 @@ class SearchAccountFragment: AccountListFragment(){
if (response.code() == 200) {
val notifications = response.body()!!.accounts as ArrayList<Account>
callback.onResult(notifications as List<Account>)
} else{
Toast.makeText(context, getString(R.string.loading_toast), Toast.LENGTH_SHORT).show()
showError()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<Results>, t: Throwable) {
Toast.makeText(context,getString(R.string.feed_failed), Toast.LENGTH_SHORT).show()
showError(errorText = R.string.feed_failed)
Log.e("FeedFragment", t.toString())
}
})

View File

@ -29,7 +29,7 @@ class SearchHashtagFragment: FeedFragment(){
private lateinit var query: String
private lateinit var content: LiveData<PagedList<Tag>>
protected lateinit var adapter : TagsRecyclerViewAdapter
private lateinit var adapter : TagsRecyclerViewAdapter
lateinit var factory: FeedDataSourceFactory<Int, Tag>
@ -59,6 +59,8 @@ class SearchHashtagFragment: FeedFragment(){
})
swipeRefreshLayout.setOnRefreshListener {
showError(show = false)
//by invalidating data, loadInitial will be called again
factory.liveData.value!!.invalidate()
}
@ -78,7 +80,7 @@ class SearchHashtagFragment: FeedFragment(){
}
private fun searchMakeAfterCall(requestedLoadSize: Int, key: Int): Call<Results> {
return pixelfedAPI
.search("Bearer $accessToken", offset=key,
.search("Bearer $accessToken", offset = key.toString(),
limit="$requestedLoadSize", q = query,
type = Results.SearchType.hashtags)
}
@ -107,14 +109,14 @@ class SearchHashtagFragment: FeedFragment(){
callback.onResult(notifications as List<Tag>)
} else{
Toast.makeText(context,getString(R.string.loading_toast), Toast.LENGTH_SHORT).show()
showError()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<Results>, t: Throwable) {
Toast.makeText(context,getString(R.string.feed_failed), Toast.LENGTH_SHORT).show()
showError(errorText = R.string.feed_failed)
Log.e("FeedFragment", t.toString())
}
})

View File

@ -8,6 +8,7 @@ import android.view.ViewGroup
import androidx.lifecycle.LiveData
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.h.pixeldroid.R
import com.h.pixeldroid.fragments.feeds.FeedFragment
import com.h.pixeldroid.fragments.feeds.postFeeds.PostsFeedFragment
import com.h.pixeldroid.objects.Results
@ -68,15 +69,15 @@ class SearchPostsFragment: PostsFeedFragment(){
if (response.code() == 200) {
val notifications = response.body()!!.statuses as ArrayList<Status>
callback.onResult(notifications as List<Status>)
} else{
Log.e("FeedFragment", "got response code ${response.code()}")
} else {
showError()
}
swipeRefreshLayout.isRefreshing = false
loadingIndicator.visibility = View.GONE
}
override fun onFailure(call: Call<Results>, t: Throwable) {
showError(errorText = R.string.feed_failed)
Log.e("FeedFragment", t.toString())
}
})

View File

@ -18,29 +18,29 @@ https://docs.joinmastodon.org/entities/account/
data class Account(
//Base attributes
override val id: String,
val username: String,
val acct: String = "",
val url: String = "", //HTTPS URL
override val id: String?,
val username: String?,
val acct: String? = "",
val url: String? = "", //HTTPS URL
//Display attributes
val display_name: String = "",
val note: String = "", //HTML
val avatar: String = "", //URL
val avatar_static: String = "", //URL
val header: String = "", //URL
val header_static: String = "", //URL
val locked: Boolean = false,
val display_name: String? = "",
val note: String? = "", //HTML
val avatar: String? = "", //URL
val avatar_static: String? = "", //URL
val header: String? = "", //URL
val header_static: String? = "", //URL
val locked: Boolean? = false,
val emojis: List<Emoji>? = null,
val discoverable: Boolean = true,
val discoverable: Boolean? = true,
//Statistical attributes
val created_at: String = "", //ISO 8601 Datetime (maybe can use a date type)
val statuses_count: Int = 0,
val followers_count: Int = 0,
val following_count: Int = 0,
val created_at: String? = "", //ISO 8601 Datetime (maybe can use a date type)
val statuses_count: Int? = 0,
val followers_count: Int? = 0,
val following_count: Int? = 0,
//Optional attributes
val moved: Account? = null,
val fields: List<Field>? = emptyList(),
val bot: Boolean = false,
val bot: Boolean? = false,
val source: Source? = null
) : Serializable, FeedContent() {
companion object {
@ -48,6 +48,7 @@ data class Account(
const val ACCOUNT_ID_TAG = "AccountIdTag"
const val FOLLOWERS_TAG = "FollowingTag"
/**
* @brief Opens an activity of the profile with the given id
*/
@ -76,6 +77,12 @@ data class Account(
}
}
fun getDisplayName() : String = when {
username.isNullOrBlank() && display_name.isNullOrBlank() -> ""
display_name.isNullOrBlank() -> "@$username"
else -> display_name.orEmpty()
}
/**
* @brief Open profile activity with given account
*/

View File

@ -16,6 +16,6 @@ data class Notification(
val status: Status? = null
): FeedContent() {
enum class NotificationType {
follow, mention, reblog, favourite
follow, mention, reblog, favourite, poll
}
}

View File

@ -1,3 +1,5 @@
package com.h.pixeldroid.objects
class Source
import java.io.Serializable
class Source: Serializable

View File

@ -109,12 +109,6 @@ data class Status(
private fun getDescription(api: PixelfedAPI, context: Context, credential: String) : Spanned =
parseHTMLText(content ?: "", mentions, api, context, credential)
fun getUsername() : CharSequence = when {
account?.username.isNullOrBlank() && account?.display_name.isNullOrBlank() -> "No Name"
account!!.username.isNullOrBlank() -> account.display_name as CharSequence
else -> account.username as CharSequence
}
fun getNLikes(context: Context) : CharSequence {
return context.getString(R.string.likes).format(favourites_count.toString())
}
@ -229,13 +223,13 @@ data class Status(
) {
//Setup username as a button that opens the profile
rootView.findViewById<TextView>(R.id.username).apply {
text = this@Status.getUsername()
text = this@Status.account?.getDisplayName() ?: ""
setTypeface(null, Typeface.BOLD)
setOnClickListener { account?.openProfile(rootView.context) }
}
rootView.findViewById<TextView>(R.id.usernameDesc).apply {
text = this@Status.getUsername()
text = this@Status.account?.getDisplayName() ?: ""
setTypeface(null, Typeface.BOLD)
}

View File

@ -25,12 +25,12 @@ class DBUtils {
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true, accessToken: String) {
db.userDao().insertUser(
UserDatabaseEntity(
user_id = account.id,
user_id = account.id!!,
//make sure not to normalize to https when localhost, to allow testing
instance_uri = normalizeOrNot(instance_uri),
username = account.username,
display_name = account.display_name,
avatar_static = account.avatar_static,
username = account.username!!,
display_name = account.getDisplayName(),
avatar_static = account.avatar_static.orEmpty(),
isActive = activeUser,
accessToken = accessToken
)
@ -68,7 +68,7 @@ class DBUtils {
instance_uri = user.instance_uri,
uri = post.uri ?: "",
account_profile_picture = post.getProfilePicUrl() ?: "",
account_name = post.getUsername().toString(),
account_name = (post.account?.getDisplayName() ?: "").toString(),
media_urls = post.media_attachments.map {
attachment -> attachment.url ?: ""
},

View File

@ -177,7 +177,7 @@ abstract class PostUtils {
holder.commentIn.visibility = View.GONE
//Add the comment to the comment section
addComment(holder.context, holder.commentCont, resp.account!!.username,
addComment(holder.context, holder.commentCont, resp.account!!.username!!,
resp.content!!
)
@ -222,7 +222,7 @@ abstract class PostUtils {
//Create the new views for each comment
for (status in statuses) {
addComment(holder.context, holder.commentCont, status.account!!.username,
addComment(holder.context, holder.commentCont, status.account!!.username!!,
status.content!!
)
}

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="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,17L7,17v-7h2v7zM13,17h-2L11,7h2v10zM17,17h-2v-4h2v4z"
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="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z"/>
</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="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,17L7,17v-7h2v7zM13,17h-2L11,7h2v10zM17,17h-2v-4h2v4z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,81 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="80dp"
android:viewportWidth="64"
android:viewportHeight="80">
<path
android:fillColor="#FF000000"
android:pathData="M42,54H40c0,-5.646 -7.311,-14 -23,-14V38C34.242,38 42,47.356 42,54Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M21.0394,38.7261l1.9971,-7.0007l1.9242,0.5489l-1.9971,7.0007z"/>
<path
android:fillColor="#FF000000"
android:pathData="M32,24a2.025,2.025 0,0 1,-1.568 -0.721l-1,-1.2a1.818,1.818 0,0 1,-0.26 -1.954A1.984,1.984 0,0 1,31 19h2a1.984,1.984 0,0 1,1.827 1.127,1.818 1.818,0 0,1 -0.259,1.953l-1,1.2A2.026,2.026 0,0 1,32 24ZM31.132,21 L31.968,22 32.905,20.943Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M37.394,20a1.376,1.376 0,0 1,-1 -0.4c-0.669,-0.669 -0.48,-1.829 0.448,-2.756s2.087,-1.117 2.756,-0.448 0.48,1.829 -0.448,2.756A2.589,2.589 0,0 1,37.394 20Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M26.606,20a2.589,2.589 0,0 1,-1.76 -0.846c-0.928,-0.927 -1.117,-2.087 -0.448,-2.756s1.828,-0.481 2.756,0.448 1.117,2.087 0.448,2.756A1.376,1.376 0,0 1,26.606 20Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M50.707,15.707l-1.414,-1.414c2,-2 2.595,-8.817 2.691,-12.293H50c-7.287,0 -9.3,1.71 -9.314,1.728a1.039,1.039 0,0 1,-1.031 0.21A19.494,19.494 0,0 0,33 3H31V1h2a22.413,22.413 0,0 1,6.834 0.886C40.9,1.216 43.713,0 50,0h3a1,1 0,0 1,1 1C54,2.168 53.907,12.507 50.707,15.707Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M34,34H30V32h4c14.617,0 18.019,-6.558 18.788,-9.11a4.9,4.9 0,0 1,-2.495 -1.183A1,1 0,0 1,51 20a3.81,3.81 0,0 0,2.221 -1.388c-2.275,-0.976 -4.387,-3.662 -6.6,-6.472l-0.41,-0.521A1,1 0,0 1,46 11C46,8.514 49.383,3.2 52.293,0.293l1.414,1.414c-2.766,2.766 -5.38,7.168 -5.679,8.982l0.169,0.215C50.444,13.762 52.991,17 55,17a1,1 0,0 1,0.832 1.555,10.9 10.9,0 0,1 -2.254,2.426 1.7,1.7 0,0 1,1.16 0.344,1 1,0 0,1 0.258,0.766C54.952,22.577 53.687,34 34,34Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M13.293,15.707C10.093,12.507 10,2.168 10,1a1,1 0,0 1,1 -1h3c6.287,0 9.1,1.216 10.166,1.886A22.413,22.413 0,0 1,31 1V3a19.494,19.494 0,0 0,-6.655 0.938,0.992 0.992,0 0,1 -1.052,-0.231h0S21.281,2 14,2H12.016c0.1,3.476 0.689,10.291 2.691,12.293Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M30,34C10.313,34 9.048,22.577 9,22.091A1,1 0,0 1,10 21c0.147,0 0.289,-0.007 0.422,-0.019a10.9,10.9 0,0 1,-2.254 -2.426A1,1 0,0 1,9 17c2.009,0 4.556,-3.238 6.8,-6.1l0.169,-0.215c-0.3,-1.814 -2.913,-6.216 -5.679,-8.982L11.707,0.293C14.617,3.2 18,8.514 18,11a1,1 0,0 1,-0.215 0.619l-0.41,0.521c-2.209,2.809 -4.319,5.494 -6.593,6.471A3.824,3.824 0,0 0,13 20a1,1 0,0 1,0.707 1.707,4.908 4.908,0 0,1 -2.5,1.184C11.979,25.438 15.377,32 30,32Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M40,64H14C4.972,64 0,59.383 0,51c0,-7.411 7.309,-13 17,-13v2C8.448,40 2,44.729 2,51c0,9.092 6.525,11 12,11H40c4.087,0 10,-2.493 10,-7V53c0,-1.638 -0.424,-2.351 -1.469,-3.949a0.8,0.8 0,0 1,-0.054 -0.095l-8.369,-16.5 1.784,-0.9L50.235,48A7.987,7.987 0,0 1,52 53v2C52,60.872 45.031,64 40,64Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M17.99,11.109A0.518,0.518 0,0 0,18 11H16c0,-1.488 5.386,-9 8,-9V4C22.744,4.113 18.309,9.836 17.99,11.109Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M46.01,11.109c-0.319,-1.273 -4.754,-7 -6.017,-7.109L40,2c2.614,0 8,7.512 8,9H46A0.518,0.518 0,0 0,46.01 11.109Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M32,34c-3.5,0 -10,0 -10,-6 0,-3.406 4.336,-7.849 8.754,-8.97l0.492,1.94C27.514,21.916 24,25.732 24,28c0,3.7 3.623,4 8,4s8,-0.3 8,-4c0,-2.265 -3.514,-6.081 -7.247,-7.031l0.494,-1.938C37.664,20.155 42,24.6 42,28 42,34 35.495,34 32,34Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M37,16a2.754,2.754 0,0 1,-3 -2.5A2.754,2.754 0,0 1,37 11a2.754,2.754 0,0 1,3 2.5A2.754,2.754 0,0 1,37 16ZM37,13c-0.62,0 -1,0.323 -1,0.5s0.38,0.5 1,0.5 1,-0.323 1,-0.5S37.62,13 37,13Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M27,16a2.754,2.754 0,0 1,-3 -2.5A2.754,2.754 0,0 1,27 11a2.754,2.754 0,0 1,3 2.5A2.754,2.754 0,0 1,27 16ZM27,13c-0.62,0 -1,0.323 -1,0.5s0.38,0.5 1,0.5 1,-0.323 1,-0.5S27.62,13 27,13Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M42.956,30a1,1 0,0 1,-0.875 -1.485c0.789,-1.421 1.11,-2.244 0.932,-3.357 -0.1,-0.632 -0.306,-1.075 -0.547,-1.187a2.132,2.132 0,0 1,-0.819 -0.721,3.262 3.262,0 0,1 -0.476,-2.984c0.354,-1.034 1.3,-2.266 3.8,-2.266C47.683,18 49,19.517 49,22.637 49,26.6 45.677,30 42.956,30ZM44.974,20c-1.6,0 -1.818,0.642 -1.912,0.916a1.263,1.263 0,0 0,0.2 1.153,0.654 0.654,0 0,0 0.1,0.121 3.237,3.237 0,0 1,1.623 2.653,5.327 5.327,0 0,1 -0.109,2.264A6.114,6.114 0,0 0,47 22.637C47,20.184 46.2,20 44.974,20Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M21.044,30C18.323,30 15,26.6 15,22.637 15,19.517 16.317,18 19.026,18c2.5,0 3.449,1.232 3.8,2.266a3.265,3.265 0,0 1,-0.478 2.987,2.135 2.135,0 0,1 -0.823,0.721c-0.235,0.109 -0.44,0.552 -0.541,1.183 -0.178,1.114 0.143,1.937 0.932,3.358A1,1 0,0 1,21.044 30ZM19.026,20C17.8,20 17,20.184 17,22.637a6.114,6.114 0,0 0,2.122 4.47,5.332 5.332,0 0,1 -0.109,-2.265 3.285,3.285 0,0 1,1.652 -2.672s0.026,-0.036 0.07,-0.1a1.265,1.265 0,0 0,0.2 -1.156C20.844,20.642 20.623,20 19.026,20Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M31,23h2v2h-2z"/>
<path
android:fillColor="#FF000000"
android:pathData="M34.445,27.832 L32,26.2l-2.445,1.63 -1.11,-1.664 3,-2a1,1 0,0 1,1.11 0l3,2Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M14,64c-4.363,0 -9,-4.557 -9,-13 0,-5.508 2.054,-10 5.494,-12.009l1.01,1.727C8.684,42.366 7,46.21 7,51c0,6.886 3.56,11 7,11Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M22,64c-4.363,0 -9,-4.557 -9,-13s4.637,-13 9,-13v2c-3.44,0 -7,4.114 -7,11s3.56,11 7,11Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M29,64c-4.363,0 -9,-4.557 -9,-13 0,-5.508 2.054,-10 5.494,-12.009l1.01,1.727C23.684,42.366 22,46.21 22,51c0,6.886 3.56,11 7,11Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M37,64c-4.363,0 -9,-4.557 -9,-13 0,-4.888 1.539,-7.923 2.83,-9.608l1.588,1.216C31.314,44.048 30,46.673 30,51c0,6.886 3.56,11 7,11Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M45,63c-4.479,0 -9,-5.257 -9,-17h2c0,9.848 3.521,15 7,15Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M37,36h2v11h-2z"/>
</vector>

View File

@ -1,164 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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:id="@+id/profileRefreshLayout"
tools:context=".ProfileActivity">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/profilePictureImageView"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/nbPostsTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center"
android:text="@string/default_nposts"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
<TextView
android:id="@+id/nbFollowersTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/default_nfollowers"
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
app:layout_constraintTop_toTopOf="@+id/nbPostsTextView" />
<TextView
android:id="@+id/nbFollowingTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="@string/default_nfollowing"
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
<TextView
android:id="@+id/accountNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
android:text="@string/no_username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/descriptionTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountNameTextView"/>
<ImageView
android:id="@+id/profilePictureImageView"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_weight="1"
tools:srcCompat="@tools:sample/avatars" />
<Button
android:id="@+id/followButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/colorButtonBg"
android:text="@string/follow"
android:textColor="@color/colorButtonText"
android:visibility="invisible"
tools:layout_editor_absoluteX="16dp"
tools:layout_editor_absoluteY="185dp"
tools:visibility="visible"
app:layout_constraintStart_toStartOf="@+id/profilePictureImageView"
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="10"
android:orientation="horizontal">
<Button
android:id="@+id/editButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:backgroundTint="@color/colorButtonBg"
android:text="@string/edit_profile"
android:textColor="@color/colorButtonText"
android:visibility="gone"
app:icon="@drawable/ic_baseline_open_in_browser_24"
app:iconTint="@color/colorButtonText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/profilePictureImageView"
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
<TextView
android:id="@+id/nbPostsTextView"
android:layout_width="15dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="@string/default_nposts" />
<TextView
android:id="@+id/nbFollowersTextView"
android:layout_width="15dp"
android:layout_height="120dp"
android:layout_weight="1"
android:gravity="center"
android:text="@string/default_nfollowers" />
<ProgressBar
android:id="@+id/profileProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/followButton" />
<TextView
android:id="@+id/nbFollowingTextView"
android:layout_width="15dp"
android:layout_height="120dp"
android:layout_weight="1"
android:gravity="center"
android:text="@string/default_nfollowing" />
</LinearLayout>
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:visibility="visible"
app:layoutDescription="@xml/error_layout_xml_error_scene"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/followButton"
tools:visibility="visible">
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:layout_marginRight="20dp">
<TextView
android:id="@+id/accountNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/no_username"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="10dp">
<TextView
android:id="@+id/descriptionTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:layout_marginBottom="15dp">
<Button
android:id="@+id/followButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/follow"
android:visibility="invisible"
android:textColor="@color/colorButtonText"
android:backgroundTint="@color/colorButtonBg"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/editButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/colorButtonText"
android:backgroundTint="@color/colorButtonBg"
android:visibility="gone"
android:text="@string/edit_profile"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/postsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@color/colorPrimaryTab"
android:src="@android:drawable/ic_dialog_dialer"
android:contentDescription="TODO" />
<ImageButton
android:id="@+id/collectionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@color/colorPrimaryTab"
android:src="@android:drawable/ic_menu_gallery"
android:contentDescription="TODO" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/error_layout"
tools:layout_editor_absoluteX="50dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/profilePostsRecyclerView"
@ -167,9 +150,11 @@
android:layout_margin="5dp"
android:nestedScrollingEnabled="false"
app:layoutManager="LinearLayoutManager"
tools:context=".fragments.ProfileFragment"
app:layout_constraintTop_toBottomOf="@id/errorLayout"
tools:listitem="@layout/fragment_profile_posts" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/errorLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
tools:visibility="visible"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:showIn="@layout/fragment_feed"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/imageView4"
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/red_panda"
app:tint="@color/colorDrawing" />
<TextView
android:id="@+id/error_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/something_went_wrong"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView4" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/panda_pull_to_refresh_to_try_again"
android:textStyle="italic"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/error_text" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,22 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layoutManager="LinearLayoutManager" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@ -24,5 +28,11 @@
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include layout="@layout/error_layout"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,6 +2,7 @@
<androidx.gridlayout.widget.GridLayout
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="wrap_content"
app:columnCount="3">
@ -9,19 +10,35 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/postPreview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="TODO"
android:padding="1dp"
app:layout_columnWeight="1"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_gravity="fill"
app:layout_rowWeight="1"
app:layout_columnWeight="1"/>
tools:srcCompat="@tools:sample/avatars" />
<ImageView
android:id="@+id/albumIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:foreground="@drawable/album_black_24dp"
android:foregroundGravity="center"
android:foregroundTint="#FFFFFF"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.gridlayout.widget.GridLayout>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView 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:id="@+id/list"
android:name="com.h.pixeldroid.fragments.ProfilePostsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".fragments.ProfilePostsFragment"
tools:listitem="@layout/fragment_profile_posts" />

View File

@ -1,7 +1,6 @@
<?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="wrap_content"
android:layout_gravity="center_horizontal">
@ -40,24 +39,36 @@
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
app:layoutDescription="@xml/error_layout_xml_error_scene">
<include layout="@layout/error_layout"/>
<TextView
android:id="@+id/discoverText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="@string/discover"
android:layout_gravity="center_horizontal"/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/errorLayout" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/discoverList"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"/>
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/discoverText" />
</LinearLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -2,15 +2,8 @@
<resources>
<color name="colorPrimary">#FFFFFF</color>
<color name="colorPrimaryTab">#6200EE</color>
<color name="colorPrimaryActionBar">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
<color name="colorButtonBg">#6200EE</color>
<color name="colorButtonText">#FFFFFF</color>
<color name="filterLabelNormal">#8A8889</color>
<color name="filterLabelSelected">#221F20</color>
<color name="colorPrimaryError">#FF0000</color>
<color name="colorText">#000000</color>
<color name="colorDrawing">#FFFFFF</color>
</resources>

View File

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

View File

@ -23,6 +23,8 @@
<string name="mention_notification">%1$s mentioned you</string>
<string name="shared_notification">%1$s shared your post</string>
<string name="liked_notification">%1$s liked your post</string>
<string name="poll_notification">"%1$s's poll has ended"</string>
<!-- Login page -->
<string name="whats_an_instance">"What's an instance?"</string>
<string name="domain_of_your_instance">Domain of your instance</string>
@ -135,6 +137,8 @@
<string name="profile_picture">Profile picture</string>
<string name="open_drawer_menu">Open drawer menu</string>
<string name="discover">DISCOVER</string>
<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>
</resources>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<Transition
android:id="@+id/transition"
app:constraintSetEnd="@+id/end"
app:constraintSetStart="@id/start"
app:duration="500">
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/errorLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="parent" />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/errorLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
</MotionScene>

View File

@ -1,13 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.0'
ext.kotlin_version = '1.4.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong