From 085a1f548c21160b90a360e9cc20da77cdf120ad Mon Sep 17 00:00:00 2001
From: Matthieu <24-artectrex@users.noreply.shinice.net>
Date: Sun, 30 Oct 2022 11:19:52 +0100
Subject: [PATCH] Implement collections
---
app/src/main/AndroidManifest.xml | 1 +
.../profile/CollectionsContentRepository.kt | 32 +++
.../profile/CollectionsPagingSource.kt | 32 +++
.../profile/ProfileContentRepository.kt | 8 +-
.../profile/ProfilePagingSource.kt | 15 +-
.../app/profile/CollectionActivity.kt | 141 +++++++++++
.../pixeldroid/app/profile/ProfileActivity.kt | 44 ++--
.../app/profile/ProfileFeedFragment.kt | 223 ++++++++++++++++--
.../java/org/pixeldroid/app/utils/Utils.kt | 2 +-
.../pixeldroid/app/utils/api/PixelfedAPI.kt | 28 +++
.../app/utils/api/objects/Collection.kt | 22 ++
app/src/main/res/drawable/collection_add.xml | 5 +
app/src/main/res/drawable/collections.xml | 6 +
.../main/res/layout/activity_collection.xml | 14 ++
.../main/res/layout/create_new_collection.xml | 19 ++
app/src/main/res/menu/collection_menu.xml | 15 ++
app/src/main/res/values/strings.xml | 16 ++
17 files changed, 574 insertions(+), 49 deletions(-)
create mode 100644 app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/CollectionsContentRepository.kt
create mode 100644 app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/CollectionsPagingSource.kt
create mode 100644 app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt
create mode 100644 app/src/main/java/org/pixeldroid/app/utils/api/objects/Collection.kt
create mode 100644 app/src/main/res/drawable/collection_add.xml
create mode 100644 app/src/main/res/drawable/collections.xml
create mode 100644 app/src/main/res/layout/activity_collection.xml
create mode 100644 app/src/main/res/layout/create_new_collection.xml
create mode 100644 app/src/main/res/menu/collection_menu.xml
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f73429e1..128d4462 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -86,6 +86,7 @@
android:name=".profile.ProfileActivity"
android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" />
+
{
+ override fun getStream(): Flow> {
+ return Pager(
+ config = PagingConfig(
+ initialLoadSize = NETWORK_PAGE_SIZE,
+ pageSize = NETWORK_PAGE_SIZE),
+ pagingSourceFactory = {
+ CollectionsPagingSource(api, accountId)
+ }
+ ).flow
+ }
+
+ companion object {
+ private const val NETWORK_PAGE_SIZE = 20
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/CollectionsPagingSource.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/CollectionsPagingSource.kt
new file mode 100644
index 00000000..37248bf9
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/CollectionsPagingSource.kt
@@ -0,0 +1,32 @@
+package org.pixeldroid.app.posts.feeds.uncachedFeeds.profile
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import org.pixeldroid.app.utils.api.PixelfedAPI
+import org.pixeldroid.app.utils.api.objects.Collection
+import retrofit2.HttpException
+import java.io.IOException
+
+class CollectionsPagingSource(
+ private val api: PixelfedAPI,
+ private val accountId: String,
+) : PagingSource() {
+ override suspend fun load(params: LoadParams): LoadResult {
+ return try {
+ val posts = api.accountCollections(accountId)
+
+ LoadResult.Page(
+ data = posts,
+ prevKey = null,
+ //TODO pagination. For now, don't paginate
+ nextKey = null
+ )
+ } catch (exception: HttpException) {
+ LoadResult.Error(exception)
+ } catch (exception: IOException) {
+ LoadResult.Error(exception)
+ }
+ }
+
+ override fun getRefreshKey(state: PagingState): String? = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/ProfileContentRepository.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/ProfileContentRepository.kt
index 9a3fd845..caf8159c 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/ProfileContentRepository.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/ProfileContentRepository.kt
@@ -14,7 +14,8 @@ class ProfileContentRepository @ExperimentalPagingApi
@Inject constructor(
private val api: PixelfedAPI,
private val accountId: String,
- private val bookmarks: Boolean
+ private val bookmarks: Boolean,
+ private val collectionId: String?,
) : UncachedContentRepository {
override fun getStream(): Flow> {
return Pager(
@@ -22,8 +23,9 @@ class ProfileContentRepository @ExperimentalPagingApi
initialLoadSize = NETWORK_PAGE_SIZE,
pageSize = NETWORK_PAGE_SIZE),
pagingSourceFactory = {
- ProfilePagingSource(api, accountId, bookmarks)
- }
+ ProfilePagingSource(api, accountId, bookmarks, collectionId)
+ },
+ initialKey = if(collectionId != null) "1" else null
).flow
}
diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt
index 282eba32..2fe0fa5a 100644
--- a/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt
+++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/uncachedFeeds/profile/ProfilePagingSource.kt
@@ -10,13 +10,20 @@ import java.io.IOException
class ProfilePagingSource(
private val api: PixelfedAPI,
private val accountId: String,
- private val bookmarks: Boolean
+ private val bookmarks: Boolean,
+ private val collectionId: String?,
) : PagingSource() {
override suspend fun load(params: LoadParams): LoadResult {
val position = params.key
return try {
val posts =
- if(bookmarks) {
+ if(collectionId != null){
+ api.collectionItems(
+ collectionId,
+ page = position
+ )
+ }
+ else if(bookmarks) {
api.bookmarks(
limit = params.loadSize,
max_id = position
@@ -34,7 +41,9 @@ class ProfilePagingSource(
LoadResult.Page(
data = posts,
prevKey = null,
- nextKey = if(nextKey == position) null else nextKey
+ nextKey = if(collectionId != null ) {
+ if(posts.isEmpty()) null else (params.key?.toIntOrNull()?.plus(1))?.toString()
+ } else if(nextKey == position) null else nextKey
)
} catch (exception: HttpException) {
LoadResult.Error(exception)
diff --git a/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt
new file mode 100644
index 00000000..b42e1918
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/profile/CollectionActivity.kt
@@ -0,0 +1,141 @@
+package org.pixeldroid.app.profile
+
+import android.app.AlertDialog
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.launch
+import org.pixeldroid.app.R
+import org.pixeldroid.app.databinding.ActivityCollectionBinding
+import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION
+import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION_ID
+import org.pixeldroid.app.utils.BaseThemedWithBarActivity
+import org.pixeldroid.app.utils.api.PixelfedAPI
+import org.pixeldroid.app.utils.api.objects.Collection
+import retrofit2.HttpException
+import java.io.IOException
+
+class CollectionActivity : BaseThemedWithBarActivity() {
+ private lateinit var binding: ActivityCollectionBinding
+
+ private lateinit var collection: Collection
+ private var addCollection: Boolean = false
+ private var deleteFromCollection: Boolean = false
+
+ companion object {
+ const val COLLECTION_TAG = "Collection"
+ const val ADD_COLLECTION_TAG = "AddCollection"
+ const val DELETE_FROM_COLLECTION_TAG = "DeleteFromCollection"
+ const val DELETE_FROM_COLLECTION_RESULT = "DeleteFromCollectionResult"
+ const val ADD_TO_COLLECTION_RESULT = "AddToCollectionResult"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityCollectionBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+ collection = intent.getSerializableExtra(COLLECTION_TAG) as Collection
+
+
+ addCollection = intent.getBooleanExtra(ADD_COLLECTION_TAG, false)
+ deleteFromCollection = intent.getBooleanExtra(DELETE_FROM_COLLECTION_TAG, false)
+
+ val addedResult = intent.getBooleanExtra(ADD_TO_COLLECTION_RESULT, false)
+ val deletedResult = intent.getBooleanExtra(DELETE_FROM_COLLECTION_RESULT, false)
+
+ if(addedResult)
+ Snackbar.make(
+ binding.root, getString(R.string.added_post_to_collection),
+ Snackbar.LENGTH_LONG
+ ).show()
+ else if (deletedResult) Snackbar.make(
+ binding.root, getString(R.string.removed_post_from_collection),
+ Snackbar.LENGTH_LONG
+ ).show()
+
+ supportActionBar?.title = if(addCollection) getString(R.string.add_to_collection)
+ else if(deleteFromCollection) getString(R.string.delete_from_collection)
+ else getString(R.string.collection_title).format(collection.username)
+
+ val collectionFragment = ProfileFeedFragment()
+ collectionFragment.arguments = Bundle().apply {
+ putBoolean(COLLECTION, true)
+ putString(COLLECTION_ID, collection.id)
+ putSerializable(COLLECTION, collection)
+ if(addCollection) putBoolean(ADD_COLLECTION_TAG, true)
+ else if (deleteFromCollection) putBoolean(DELETE_FROM_COLLECTION_TAG, true)
+ }
+
+ supportFragmentManager.beginTransaction()
+ .add(R.id.collectionFragment, collectionFragment).commit()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ val userId = db.userDao().getActiveUser()?.user_id
+
+ // Only show options for editing a collection if it's the user's collection
+ if(!(addCollection || deleteFromCollection) && userId != null && collection.pid == userId) {
+ val inflater: MenuInflater = menuInflater
+ inflater.inflate(R.menu.collection_menu, menu)
+ }
+ return true
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ // Relaunch same activity, to avoid duplicates in history
+ super.onNewIntent(intent);
+ finish();
+ startActivity(intent);
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.delete_collection -> {
+ AlertDialog.Builder(this).apply {
+ setMessage(R.string.delete_collection_warning)
+ setPositiveButton(android.R.string.ok) { _, _ ->
+ // Delete collection
+ lifecycleScope.launch {
+ val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
+ try {
+ api.deleteCollection(collection.id)
+ // Deleted, exit activity
+ finish()
+ } catch (exception: IOException) {
+ TODO("Error")
+ } catch (exception: HttpException) {
+ TODO("Error")
+ }
+ }
+ }
+ setNegativeButton(android.R.string.cancel) { _, _ -> }
+ }.show()
+ true
+ }
+ R.id.add_post_collection -> {
+ val intent = Intent(this, CollectionActivity::class.java)
+ intent.putExtra(COLLECTION_TAG, collection)
+ intent.putExtra(ADD_COLLECTION_TAG, true)
+ startActivity(intent)
+ true
+ }
+ R.id.remove_post_collection -> {
+ val intent = Intent(this, CollectionActivity::class.java)
+ intent.putExtra(COLLECTION_TAG, collection)
+ intent.putExtra(DELETE_FROM_COLLECTION_TAG, true)
+ startActivity(intent)
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+
+}
diff --git a/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt
index 44ce47fe..d85b9a08 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/ProfileActivity.kt
@@ -65,40 +65,44 @@ class ProfileActivity : BaseThemedWithBarActivity() {
private fun createProfileTabs(account: Account?): Array{
val profileFeedFragment = ProfileFeedFragment()
- val argumentsFeed = Bundle().apply {
+ profileFeedFragment.arguments = Bundle().apply {
putSerializable(Account.ACCOUNT_TAG, account)
putSerializable(ProfileFeedFragment.PROFILE_GRID, false)
putSerializable(ProfileFeedFragment.BOOKMARKS, false)
}
- profileFeedFragment.arguments = argumentsFeed
val profileGridFragment = ProfileFeedFragment()
- val argumentsGrid = Bundle().apply {
+ profileGridFragment.arguments = Bundle().apply {
putSerializable(Account.ACCOUNT_TAG, account)
putSerializable(ProfileFeedFragment.PROFILE_GRID, true)
putSerializable(ProfileFeedFragment.BOOKMARKS, false)
}
- profileGridFragment.arguments = argumentsGrid
+
+ val profileCollectionsFragment = ProfileFeedFragment()
+ profileCollectionsFragment.arguments = Bundle().apply {
+ putSerializable(Account.ACCOUNT_TAG, account)
+ putSerializable(ProfileFeedFragment.PROFILE_GRID, true)
+ putSerializable(ProfileFeedFragment.BOOKMARKS, false)
+ putSerializable(ProfileFeedFragment.COLLECTIONS, true)
+ }
+
+ val returnArray: Array = arrayOf(
+ profileGridFragment,
+ profileFeedFragment,
+ profileCollectionsFragment
+ )
// If we are viewing our own account, show bookmarks
if(account == null || account.id == user?.user_id) {
val profileBookmarksFragment = ProfileFeedFragment()
- val argumentsBookmarks = Bundle().apply {
+ profileBookmarksFragment.arguments = Bundle().apply {
putSerializable(Account.ACCOUNT_TAG, account)
putSerializable(ProfileFeedFragment.PROFILE_GRID, true)
putSerializable(ProfileFeedFragment.BOOKMARKS, true)
}
- profileBookmarksFragment.arguments = argumentsBookmarks
- return arrayOf(
- profileGridFragment,
- profileFeedFragment,
- profileBookmarksFragment
- )
+ return returnArray + profileBookmarksFragment
}
- return arrayOf(
- profileGridFragment,
- profileFeedFragment
- )
+ return returnArray
}
private fun setupTabs(
@@ -117,15 +121,19 @@ class ProfileActivity : BaseThemedWithBarActivity() {
tab.tabLabelVisibility = TabLayout.TAB_LABEL_VISIBILITY_UNLABELED
when (position) {
0 -> {
- tab.setText("Grid view")
+ tab.setText(R.string.grid_view)
tab.setIcon(R.drawable.grid_on_black_24dp)
}
1 -> {
- tab.setText("Feed view")
+ tab.setText(R.string.feed_view)
tab.setIcon(R.drawable.feed_view)
}
2 -> {
- tab.setText("Bookmarks")
+ tab.setText(R.string.collections)
+ tab.setIcon(R.drawable.collections)
+ }
+ 3 -> {
+ tab.setText(R.string.bookmarks)
tab.setIcon(R.drawable.bookmark)
}
}
diff --git a/app/src/main/java/org/pixeldroid/app/profile/ProfileFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/profile/ProfileFeedFragment.kt
index 45be30a6..a4ee39d3 100644
--- a/app/src/main/java/org/pixeldroid/app/profile/ProfileFeedFragment.kt
+++ b/app/src/main/java/org/pixeldroid/app/profile/ProfileFeedFragment.kt
@@ -6,36 +6,55 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
+import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
+import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentProfilePostsBinding
import org.pixeldroid.app.posts.PostActivity
import org.pixeldroid.app.posts.StatusViewHolder
-import org.pixeldroid.app.posts.feeds.UIMODEL_STATUS_COMPARATOR
import org.pixeldroid.app.posts.feeds.uncachedFeeds.*
+import org.pixeldroid.app.posts.feeds.uncachedFeeds.profile.CollectionsContentRepository
import org.pixeldroid.app.posts.feeds.uncachedFeeds.profile.ProfileContentRepository
+import org.pixeldroid.app.profile.CollectionActivity.Companion.ADD_COLLECTION_TAG
+import org.pixeldroid.app.profile.CollectionActivity.Companion.ADD_TO_COLLECTION_RESULT
+import org.pixeldroid.app.profile.CollectionActivity.Companion.DELETE_FROM_COLLECTION_RESULT
+import org.pixeldroid.app.profile.CollectionActivity.Companion.DELETE_FROM_COLLECTION_TAG
import org.pixeldroid.app.utils.BlurHashDecoder
+import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Attachment
+import org.pixeldroid.app.utils.api.objects.Collection
+import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.displayDimensionsInPx
+import org.pixeldroid.app.utils.openUrl
import org.pixeldroid.app.utils.setSquareImageFromURL
+import retrofit2.HttpException
+import java.io.IOException
/**
* Fragment to show a list of [Account]s, as a result of a search.
*/
-class ProfileFeedFragment : UncachedFeedFragment() {
+class ProfileFeedFragment : UncachedFeedFragment() {
companion object {
+ // List of collections
+ const val COLLECTIONS = "Collections"
+ // Content of collection
+ const val COLLECTION = "Collection"
+ const val COLLECTION_ID = "CollectionId"
const val PROFILE_GRID = "ProfileGrid"
const val BOOKMARKS = "Bookmarks"
}
@@ -44,12 +63,27 @@ class ProfileFeedFragment : UncachedFeedFragment() {
private var user: UserDatabaseEntity? = null
private var grid: Boolean = true
private var bookmarks: Boolean = false
+ private var collections: Boolean = false
+ private var collection: Collection? = null
+ private var addCollection: Boolean = false
+ private var deleteFromCollection: Boolean = false
+ private var collectionId: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- grid = arguments?.getSerializable(PROFILE_GRID) as Boolean
- bookmarks = arguments?.getSerializable(BOOKMARKS) as Boolean
+ grid = arguments?.getBoolean(PROFILE_GRID, true) ?: true
+ bookmarks = arguments?.getBoolean(BOOKMARKS) ?: false
+ collections = arguments?.getBoolean(COLLECTIONS) ?: false
+ collection = arguments?.getSerializable(COLLECTION) as? Collection
+ addCollection = arguments?.getBoolean(ADD_COLLECTION_TAG) ?: false
+ deleteFromCollection = arguments?.getBoolean(DELETE_FROM_COLLECTION_TAG) ?: false
+ collectionId = arguments?.getString(COLLECTION_ID)
+ if(addCollection){
+ // We want the user's profile, set all the rest to false to be sure
+ collections = false
+ bookmarks = false
+ }
adapter = ProfilePostsAdapter()
//get the currently active user
@@ -67,20 +101,23 @@ class ProfileFeedFragment : UncachedFeedFragment() {
val view = super.onCreateView(inflater, container, savedInstanceState)
- if(grid || bookmarks) {
+ if(grid || bookmarks || collections || addCollection) {
binding.list.layoutManager = GridLayoutManager(context, 3)
}
// Get the view model
@Suppress("UNCHECKED_CAST")
viewModel = ViewModelProvider(requireActivity(), ProfileViewModelFactory(
- ProfileContentRepository(
+ (if(!collections) ProfileContentRepository(
apiHolder.setToCurrentUser(),
accountId,
- bookmarks
+ bookmarks,
+ if (addCollection) null else collectionId
)
+ else CollectionsContentRepository(apiHolder.setToCurrentUser(), accountId)) as UncachedContentRepository
)
- )[if(bookmarks) "Bookmarks" else "Profile", FeedViewModel::class.java] as FeedViewModel
+ )[if (addCollection) "AddCollection" else if (collections) "Collections" else if(bookmarks) "Bookmarks" else "Profile",
+ FeedViewModel::class.java] as FeedViewModel
launch()
initSearch()
@@ -88,29 +125,122 @@ class ProfileFeedFragment : UncachedFeedFragment() {
return view
}
- inner class ProfilePostsAdapter() : PagingDataAdapter(
- UIMODEL_STATUS_COMPARATOR
+ inner class ProfilePostsAdapter : PagingDataAdapter(
+ object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: FeedContent, newItem: FeedContent): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: FeedContent, newItem: FeedContent): Boolean =
+ oldItem.id == newItem.id
+ }
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
- return if(grid || bookmarks) {
- ProfilePostsViewHolder.create(parent)
- } else {
- StatusViewHolder.create(parent)
- }
+ return if(collections) {
+ if (viewType == 1) {
+ val view =
+ LayoutInflater.from(parent.context)
+ .inflate(R.layout.create_new_collection, parent, false)
+ AddCollectionViewHolder(view)
+ } else CollectionsViewHolder.create(parent)
+ }
+ else if(grid || bookmarks) {
+ ProfilePostsViewHolder.create(parent)
+ } else {
+ StatusViewHolder.create(parent)
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if(position == 0 && user?.user_id == accountId) 1
+ else 0
+ }
+
+ override fun getItemCount(): Int {
+ return if (collections && user?.user_id == accountId) {
+ super.getItemCount() + 1
+ } else super.getItemCount()
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
- val post = getItem(position)
+ val post = if(collections && user?.user_id == accountId && position == 0) null else getItem(if(collections && user?.user_id == accountId) position - 1 else position)
post?.let {
- if(grid || bookmarks) {
- (holder as ProfilePostsViewHolder).bind(it)
+ if(collections) {
+ (holder as CollectionsViewHolder).bind(it as Collection)
+ } else if(grid || bookmarks || addCollection) {
+ (holder as ProfilePostsViewHolder).bind(
+ it as Status,
+ lifecycleScope,
+ apiHolder.api ?: apiHolder.setToCurrentUser(),
+ addCollection,
+ collection,
+ deleteFromCollection
+ )
} else {
- (holder as StatusViewHolder).bind(it, apiHolder, db,
+ (holder as StatusViewHolder).bind(it as Status, apiHolder, db,
lifecycleScope, requireContext().displayDimensionsInPx())
}
}
+
+ if(collections && post == null){
+ (holder as AddCollectionViewHolder).itemView.setOnClickListener {
+ val domain = user?.instance_uri
+ val url = "$domain/i/collections/create"
+
+ if(domain.isNullOrEmpty() || !requireContext().openUrl(url)) {
+ Snackbar.make(binding.root, getString(R.string.new_collection_link_failed),
+ Snackbar.LENGTH_LONG).show()
+ }
+ }
+
+ }
+ }
+ }
+ class AddCollectionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
+}
+
+
+class CollectionsViewHolder(binding: FragmentProfilePostsBinding) : RecyclerView.ViewHolder(binding.root) {
+ private val postPreview: ImageView = binding.postPreview
+ private val albumIcon: ImageView = binding.albumIcon
+ private val videoIcon: ImageView = binding.videoIcon
+
+ fun bind(collection: Collection) {
+
+ if (collection.post_count == 0){
+ //No media in this collection, so put a little icon there
+ postPreview.scaleX = 0.3f
+ postPreview.scaleY = 0.3f
+ Glide.with(postPreview).load(R.drawable.ic_comment_empty).into(postPreview)
+ albumIcon.visibility = View.GONE
+ videoIcon.visibility = View.GONE
+ } else {
+ postPreview.scaleX = 1f
+ postPreview.scaleY = 1f
+ setSquareImageFromURL(postPreview, collection.thumb, postPreview)
+ if (collection.post_count > 1) {
+ albumIcon.visibility = View.VISIBLE
+ } else {
+ albumIcon.visibility = View.GONE
+ }
+ videoIcon.visibility = View.GONE
+ }
+
+ postPreview.setOnClickListener {
+ val intent = Intent(postPreview.context, CollectionActivity::class.java)
+ intent.putExtra(CollectionActivity.COLLECTION_TAG, collection)
+ postPreview.context.startActivity(intent)
+ }
+ }
+
+ companion object {
+ fun create(parent: ViewGroup): CollectionsViewHolder {
+ val itemBinding = FragmentProfilePostsBinding.inflate(
+ LayoutInflater.from(parent.context), parent, false
+ )
+ return CollectionsViewHolder(itemBinding)
}
}
}
@@ -120,7 +250,9 @@ class ProfilePostsViewHolder(binding: FragmentProfilePostsBinding) : RecyclerVie
private val albumIcon: ImageView = binding.albumIcon
private val videoIcon: ImageView = binding.videoIcon
- fun bind(post: Status) {
+ fun bind(post: Status, lifecycleScope: LifecycleCoroutineScope, api: PixelfedAPI,
+ addCollection: Boolean = false, collection: Collection? = null, deleteFromCollection: Boolean = false
+ ) {
if ((post.media_attachments?.size ?: 0) == 0){
//No media in this post, so put a little icon there
@@ -158,9 +290,52 @@ class ProfilePostsViewHolder(binding: FragmentProfilePostsBinding) : RecyclerVie
}
postPreview.setOnClickListener {
- val intent = Intent(postPreview.context, PostActivity::class.java)
- intent.putExtra(Status.POST_TAG, post)
- postPreview.context.startActivity(intent)
+ if(addCollection && collection != null){
+ lifecycleScope.launch {
+ try {
+ api.addToCollection(collection.id, post.id)
+ val intent = Intent(postPreview.context, CollectionActivity::class.java)
+ .apply {
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ putExtra(ADD_TO_COLLECTION_RESULT, true)
+ putExtra(CollectionActivity.COLLECTION_TAG, collection)
+ }
+ postPreview.context.startActivity(intent)
+ } catch (exception: IOException) {
+ Snackbar.make(postPreview, postPreview.context.getString(R.string.error_add_post_to_collection),
+ Snackbar.LENGTH_LONG).show()
+ } catch (exception: HttpException) {
+ Snackbar.make(postPreview, postPreview.context.getString(R.string.error_add_post_to_collection),
+ Snackbar.LENGTH_LONG).show()
+ }
+ }
+ } else if (deleteFromCollection && (collection != null)){
+ lifecycleScope.launch {
+ try {
+ api.removeFromCollection(collection.id, post.id)
+ val intent = Intent(postPreview.context, CollectionActivity::class.java)
+ .apply {
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ putExtra(DELETE_FROM_COLLECTION_RESULT, true)
+ putExtra(CollectionActivity.COLLECTION_TAG, collection)
+ }
+ postPreview.context.startActivity(intent)
+ } catch (exception: IOException) {
+ Snackbar.make(postPreview, postPreview.context.getString(R.string.error_remove_post_from_collection),
+ Snackbar.LENGTH_LONG).show()
+ } catch (exception: HttpException) {
+ Snackbar.make(postPreview, postPreview.context.getString(R.string.error_remove_post_from_collection),
+ Snackbar.LENGTH_LONG).show()
+ }
+ }
+ }
+ else {
+ val intent = Intent(postPreview.context, PostActivity::class.java)
+ intent.putExtra(Status.POST_TAG, post)
+ postPreview.context.startActivity(intent)
+ }
}
}
@@ -176,7 +351,7 @@ class ProfilePostsViewHolder(binding: FragmentProfilePostsBinding) : RecyclerVie
class ProfileViewModelFactory @ExperimentalPagingApi constructor(
- private val searchContentRepository: UncachedContentRepository
+ private val searchContentRepository: UncachedContentRepository
) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
diff --git a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt
index 2d646bcc..69090468 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt
@@ -95,7 +95,7 @@ fun normalizeDomain(domain: String): String {
.trim(Char::isWhitespace)
}
-fun BaseActivity.openUrl(url: String): Boolean {
+fun Context.openUrl(url: String): Boolean {
val intent = CustomTabsIntent.Builder().build()
diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt
index 19e383e2..2d8c11df 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt
@@ -7,6 +7,7 @@ import okhttp3.Interceptor
import org.pixeldroid.app.utils.api.objects.*
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
+import org.pixeldroid.app.utils.api.objects.Collection
import org.pixeldroid.app.utils.api.objects.Tag
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@@ -201,6 +202,33 @@ interface PixelfedAPI {
@Path("id") statusId: String
) : Status
+ @GET("/api/v1.1/collections/accounts/{id}")
+ suspend fun accountCollections(
+ @Path("id") account_id: String? = null
+ ): List
+
+ @GET("/api/v1.1/collections/items/{id}")
+ suspend fun collectionItems(
+ @Path("id") id: String,
+ @Query("page") page: String? = null
+ ): List
+
+ @DELETE("/api/v1.1/collections/delete/{id}")
+ suspend fun deleteCollection(
+ @Path("id") id: String,
+ )
+
+ @POST("/api/v1.1/collections/add")
+ suspend fun addToCollection(
+ @Query("collection_id") collection_id: String,
+ @Query("post_id") post_id: String,
+ ): Status
+
+ @POST("/api/v1.1/collections/remove")
+ suspend fun removeFromCollection(
+ @Query("collection_id") collection_id: String,
+ @Query("post_id") post_id: String,
+ )
//Used in our case to retrieve comments for a given status
@GET("/api/v1/statuses/{id}/context")
diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Collection.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Collection.kt
new file mode 100644
index 00000000..799a8cd3
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Collection.kt
@@ -0,0 +1,22 @@
+package org.pixeldroid.app.utils.api.objects
+
+import java.io.Serializable
+import java.time.Instant
+
+data class Collection(
+ override val id: String, // Id of the profile
+ val pid: String, // Account id
+ val visibility: Visibility, // Public or private, or draft for your own collections
+ val title: String,
+ val description: String,
+ val thumb: String, // URL to the thumbnail of this collection
+ val updated_at: Instant,
+ val published_at: Instant,
+ val avatar: String, // URL to the avatar of the author of this collection
+ val username: String, // Username of author
+ val post_count: Int, //Number of posts in collection
+): FeedContent, Serializable {
+ enum class Visibility: Serializable {
+ public, private, draft
+ }
+}
diff --git a/app/src/main/res/drawable/collection_add.xml b/app/src/main/res/drawable/collection_add.xml
new file mode 100644
index 00000000..ddfd24bb
--- /dev/null
+++ b/app/src/main/res/drawable/collection_add.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/collections.xml b/app/src/main/res/drawable/collections.xml
new file mode 100644
index 00000000..365f6ff7
--- /dev/null
+++ b/app/src/main/res/drawable/collections.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_collection.xml b/app/src/main/res/layout/activity_collection.xml
new file mode 100644
index 00000000..13118b3b
--- /dev/null
+++ b/app/src/main/res/layout/activity_collection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/create_new_collection.xml b/app/src/main/res/layout/create_new_collection.xml
new file mode 100644
index 00000000..f42287a3
--- /dev/null
+++ b/app/src/main/res/layout/create_new_collection.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/app/src/main/res/menu/collection_menu.xml b/app/src/main/res/menu/collection_menu.xml
new file mode 100644
index 00000000..a2df8620
--- /dev/null
+++ b/app/src/main/res/menu/collection_menu.xml
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4d9f42ee..30aa2c57 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -177,6 +177,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"
Image successfully saved
Could not get follow status
Failed to open edit page
+ Failed to open collection creation page
Nothing to see here :(
Could not display follow button
Could not follow
@@ -212,6 +213,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"
PixelDroid is free and open source software, licensed under the GNU General Public License (version 3 or later)
About
%1$s\'s post
+ %1$s\'s collection
%1$s\'s followers
#%1$s
%1$s\'s follows
@@ -289,6 +291,20 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"
View daily trending posts
Trending Posts
Explore random posts of the day
+ Grid view
+ Feed view
+ Bookmarks
+ Collections
+ Delete collection
+ Add post
+ Remove post
+ Are you sure you want to delete this collection?
+ Choose a post to add
+ Choose a post to remove
+ Added post to the collection
+ Failed to add post to the collection
+ Failed to remove post from the collection
+ Removed post from collection
- %d reply
- %d replies