Merge branch 'collections' into 'master'

Implement collections

See merge request pixeldroid/PixelDroid!492
This commit is contained in:
Matthieu 2022-10-30 11:46:55 +00:00
commit 5b3870b80d
17 changed files with 574 additions and 49 deletions

View File

@ -86,6 +86,7 @@
android:name=".profile.ProfileActivity"
android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" />
<activity android:name=".profile.CollectionActivity"/>
<activity
android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings2"

View File

@ -0,0 +1,32 @@
package org.pixeldroid.app.posts.feeds.uncachedFeeds.profile
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedContentRepository
import org.pixeldroid.app.utils.api.PixelfedAPI
import kotlinx.coroutines.flow.Flow
import org.pixeldroid.app.utils.api.objects.Collection
import javax.inject.Inject
class CollectionsContentRepository @ExperimentalPagingApi
@Inject constructor(
private val api: PixelfedAPI,
private val accountId: String,
) : UncachedContentRepository<Collection> {
override fun getStream(): Flow<PagingData<Collection>> {
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
}
}

View File

@ -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<String, Collection>() {
override suspend fun load(params: LoadParams<String>): LoadResult<String, Collection> {
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, Collection>): String? = null
}

View File

@ -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<Status> {
override fun getStream(): Flow<PagingData<Status>> {
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
}

View File

@ -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<String, Status>() {
override suspend fun load(params: LoadParams<String>): LoadResult<String, Status> {
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)

View File

@ -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)
}
}
}

View File

@ -65,40 +65,44 @@ class ProfileActivity : BaseThemedWithBarActivity() {
private fun createProfileTabs(account: Account?): Array<Fragment>{
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<Fragment> = 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)
}
}

View File

@ -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<Status>() {
class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
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<Status>() {
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<Status>() {
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<FeedContent>
)
)[if(bookmarks) "Bookmarks" else "Profile", FeedViewModel::class.java] as FeedViewModel<Status>
)[if (addCollection) "AddCollection" else if (collections) "Collections" else if(bookmarks) "Bookmarks" else "Profile",
FeedViewModel::class.java] as FeedViewModel<FeedContent>
launch()
initSearch()
@ -88,29 +125,122 @@ class ProfileFeedFragment : UncachedFeedFragment<Status>() {
return view
}
inner class ProfilePostsAdapter() : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
UIMODEL_STATUS_COMPARATOR
inner class ProfilePostsAdapter : PagingDataAdapter<FeedContent, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<FeedContent>() {
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<Status>
private val searchContentRepository: UncachedContentRepository<FeedContent>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {

View File

@ -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()

View File

@ -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<Collection>
@GET("/api/v1.1/collections/items/{id}")
suspend fun collectionItems(
@Path("id") id: String,
@Query("page") page: String? = null
): List<Status>
@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")

View File

@ -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
}
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM19,11h-4v4h-2v-4L9,11L9,9h4L13,5h2v4h4v2z"/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
<path android:fillColor="@android:color/white" android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,12l-2.5,-1.5L15,12L15,4h5v8z"/>
</vector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".searchDiscover.TrendingActivity">
<FrameLayout
android:id = "@+id/collectionFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<org.pixeldroid.app.postCreation.SquareLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/addPhotoSquare"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:backgroundTint="?attr/colorOnBackground"
android:background="@drawable/collection_add"
android:contentDescription="@string/add_photo" />
</org.pixeldroid.app.postCreation.SquareLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/delete_collection"
android:enabled="true"
android:title="@string/delete_collection"/>
<item
android:id="@+id/add_post_collection"
android:enabled="true"
android:title="@string/collection_add_post"/>
<item
android:id="@+id/remove_post_collection"
android:enabled="true"
android:title="@string/collection_remove_post"/>
</menu>

View File

@ -177,6 +177,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="save_image_success">Image successfully saved</string>
<string name="follow_status_failed">Could not get follow status</string>
<string name="edit_link_failed">Failed to open edit page</string>
<string name="new_collection_link_failed">Failed to open collection creation page</string>
<string name="empty_feed">Nothing to see here :(</string>
<string name="follow_button_failed">Could not display follow button</string>
<string name="follow_error">Could not follow</string>
@ -212,6 +213,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="license_info">PixelDroid is free and open source software, licensed under the GNU General Public License (version 3 or later)</string>
<string name="about">About</string>
<string name="post_title">%1$s\'s post</string>
<string name="collection_title">%1$s\'s collection</string>
<string name="followers_title">%1$s\'s followers</string>
<string name="hashtag_title">#%1$s</string>
<string name="follows_title">%1$s\'s follows</string>
@ -289,6 +291,20 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="daily_trending">View daily trending posts</string>
<string name="trending_posts">Trending Posts</string>
<string name="explore_posts">Explore random posts of the day</string>
<string name="grid_view">Grid view</string>
<string name="feed_view">Feed view</string>
<string name="bookmarks">Bookmarks</string>
<string name="collections">Collections</string>
<string name="delete_collection">Delete collection</string>
<string name="collection_add_post">Add post</string>
<string name="collection_remove_post">Remove post</string>
<string name="delete_collection_warning">Are you sure you want to delete this collection?</string>
<string name="add_to_collection">Choose a post to add</string>
<string name="delete_from_collection">Choose a post to remove</string>
<string name="added_post_to_collection">Added post to the collection</string>
<string name="error_add_post_to_collection">Failed to add post to the collection</string>
<string name="error_remove_post_from_collection">Failed to remove post from the collection</string>
<string name="removed_post_from_collection">Removed post from collection</string>
<plurals name="replies_count">
<item quantity="one">%d reply</item>
<item quantity="other">%d replies</item>