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