Merge branch 'blurhash_and_ratios' into 'master'

Initial blurhash use

See merge request pixeldroid/PixelDroid!303
This commit is contained in:
Matthieu 2021-02-11 00:31:48 +00:00
commit 7b7117b359
26 changed files with 303 additions and 146 deletions

View File

@ -4,6 +4,7 @@ import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_CHOOSER
import android.widget.ImageButton
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.intent.Intents
@ -14,7 +15,6 @@ import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.postCreation.camera.CameraFragment
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import kotlinx.android.synthetic.main.camera_ui_container.*
import org.hamcrest.CoreMatchers
import org.hamcrest.Matcher
import org.junit.After
@ -50,9 +50,9 @@ class CameraTest {
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
refreshToken = "refreshToken",
clientId = "clientId",
clientSecret = "clientSecret"
)
)
db.close()
@ -88,7 +88,7 @@ class CameraTest {
val scenario = launchFragmentInContainer<CameraFragment>()
scenario.onFragment { fragment ->
fragment.photo_view_button.performClick()
fragment.view?.findViewById<ImageButton>(R.id.photo_view_button)?.performClick()
}
Thread.sleep(1000)
@ -102,7 +102,7 @@ class CameraTest {
fun switchButton() {
val scenario = launchFragmentInContainer<CameraFragment>()
scenario.onFragment { fragment ->
fragment.camera_switch_button.performClick()
fragment.view?.findViewById<ImageButton>(R.id.camera_switch_button)?.performClick()
}
Thread.sleep(1000)
scenario.onFragment { fragment ->

View File

@ -61,9 +61,9 @@ class DrawerMenuTest {
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
refreshToken = "refreshToken",
clientId = "clientId",
clientSecret = "clientSecret"
)
)
db.close()
@ -132,8 +132,7 @@ class DrawerMenuTest {
onView(withText(R.string.menu_account)).perform(click())
// Check that profile activity was opened.
onView(withId(R.id.editButton)).check(matches(isDisplayed()))
val followersText = context.getString(R.string.nb_followers)
.format(68)
val followersText = context.resources.getQuantityString(R.plurals.nb_followers, 68)
onView(withText(followersText)).perform(click())
onView(withText("Dobios")).check(matches(isDisplayed()))
}
@ -144,8 +143,7 @@ class DrawerMenuTest {
onView(withText(R.string.menu_account)).perform(click())
// Check that profile activity was opened.
onView(withId(R.id.editButton)).check(matches(isDisplayed()))
val followingText = context.getString(R.string.nb_following)
.format(27)
val followingText = context.resources.getQuantityString(R.plurals.nb_followers, 27)
onView(withText(followingText)).perform(click())
onView(withText("Dobios")).check(matches(isDisplayed()))
}

View File

@ -22,14 +22,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.adapters.ThumbnailAdapter
import com.h.pixeldroid.postCreation.photoEdit.PhotoEditActivity
import com.h.pixeldroid.postCreation.photoEdit.ThumbnailAdapter
import com.h.pixeldroid.settings.AboutActivity
import com.h.pixeldroid.testUtility.CustomMatchers
import com.h.pixeldroid.testUtility.clearData
import junit.framework.Assert.assertTrue
import kotlinx.android.synthetic.main.fragment_edit_image.*
import org.hamcrest.CoreMatchers.allOf
import org.junit.*
import org.junit.Assert.assertTrue
import org.junit.rules.Timeout
import org.junit.runner.RunWith
import java.io.File
@ -141,9 +141,9 @@ class EditPhotoTest {
Espresso.onView(withId(R.id.seekbar_contrast)).perform(setProgress(change))
Espresso.onView(withId(R.id.seekbar_saturation)).perform(setProgress(change))
Assert.assertEquals(change, activity.seekbar_brightness.progress)
Assert.assertEquals(change, activity.seekbar_contrast.progress)
Assert.assertEquals(change, activity.seekbar_saturation.progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_brightness).progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_contrast).progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_saturation).progress)
Thread.sleep(1000)
@ -152,9 +152,9 @@ class EditPhotoTest {
Espresso.onView(withId(R.id.seekbar_contrast)).perform(setProgress(change))
Espresso.onView(withId(R.id.seekbar_saturation)).perform(setProgress(change))
Assert.assertEquals(change, activity.seekbar_brightness.progress)
Assert.assertEquals(change, activity.seekbar_contrast.progress)
Assert.assertEquals(change, activity.seekbar_saturation.progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_brightness).progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_contrast).progress)
Assert.assertEquals(change, activity.findViewById<SeekBar>(R.id.seekbar_saturation).progress)
}
@Test
@ -181,7 +181,7 @@ class EditPhotoTest {
@Test
fun alreadyUploadingDialog() {
activityScenario.onActivity { a -> a.saving = true }
Espresso.onView(withId(R.id.action_upload)).perform(click())
Espresso.onView(withId(R.id.action_save)).perform(click())
Thread.sleep(1000)
Espresso.onView(withText(R.string.busy_dialog_text)).check(matches(isDisplayed()))
}

View File

@ -68,9 +68,9 @@ class HomeFeedTest {
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
refreshToken = "refreshToken",
clientId = "clientId",
clientSecret = "clientSecret"
)
)
db.close()

View File

@ -85,9 +85,9 @@ class IntentTest {
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
refreshToken = "refreshToken",
clientId = "clientId",
clientSecret = "clientSecret"
)
)
db.close()

View File

@ -124,9 +124,9 @@ class LoginActivityOnlineTest {
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
refreshToken = "refreshToken",
clientId = "clientId",
clientSecret = "clientSecret"
)
)
db.close()

View File

@ -1,6 +1,5 @@
package com.h.pixeldroid
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import androidx.test.core.app.ActivityScenario
@ -28,8 +27,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginCheckIntent {
private lateinit var context: Context
@get:Rule
var globalTimeout: Timeout = Timeout.seconds(100)

View File

@ -16,13 +16,13 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity
import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity
import com.h.pixeldroid.testUtility.MockServer
import com.h.pixeldroid.testUtility.clearData
import com.h.pixeldroid.testUtility.initDB
import kotlinx.android.synthetic.main.activity_main.*
import org.hamcrest.Matcher
import org.junit.After
import org.junit.Before
@ -96,9 +96,9 @@ class PostFragmentUITests {
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
refreshToken = "refreshToken",
clientId = "clientId",
clientSecret = "clientSecret"
)
)
db.close()
@ -114,7 +114,7 @@ class PostFragmentUITests {
@Test
fun newPostUiTest() {
ActivityScenario.launch(MainActivity::class.java).onActivity {
a -> a.tabs.getTabAt(2)!!.select()
it.findViewById<TabLayout>(R.id.tabs).getTabAt(2)!!.select()
}
Thread.sleep(1500)
onView(withId(R.id.photo_view_button)).check(matches(isDisplayed()))

View File

@ -63,9 +63,9 @@ class PostTest {
avatar_static = "some_avatar_url",
isActive = true,
accessToken = "token",
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
refreshToken = "refreshToken",
clientId = "clientId",
clientSecret = "clientSecret"
)
)
db.close()
@ -76,14 +76,15 @@ class PostTest {
fun saveToGalleryTestSimplePost() {
val attachment = Attachment(
id = "12",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png",
meta = null
)
val post = Status(
id = "12",
account = Account(
id = "12",
username = "douze",
url = "https://pixelfed.de/douze"
url = "https://pixelfed.de/douze",
),
media_attachments = listOf(attachment)
)
@ -106,11 +107,13 @@ class PostTest {
fun saveToGalleryTestAlbum() {
val attachment1 = Attachment(
id = "12",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png",
meta = null
)
val attachment2 = Attachment(
id = "13",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png",
meta = null
)
val post = Status(
id = "12",
@ -141,14 +144,15 @@ class PostTest {
val expectedIntent: Matcher<Intent> = IntentMatchers.hasAction(Intent.ACTION_CHOOSER)
val attachment = Attachment(
id = "12",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png",
meta = null
)
val post = Status(
id = "12",
account = Account(
id = "12",
username = "douze",
url = "https://pixelfed.de/douze"
url = "https://pixelfed.de/douze",
),
media_attachments = listOf(attachment)
)
@ -166,11 +170,13 @@ class PostTest {
val expectedIntent: Matcher<Intent> = IntentMatchers.hasAction(Intent.ACTION_CHOOSER)
val attachment1 = Attachment(
id = "12",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png",
meta = null
)
val attachment2 = Attachment(
id = "13",
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png"
url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png",
meta = null
)
val post = Status(
id = "12",
@ -208,7 +214,7 @@ class PostTest {
media_attachments= listOf(
Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)
remote_url=null, text_url=null, description=null, blurhash=null, meta = null)
),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),
@ -235,7 +241,7 @@ class PostTest {
media_attachments= listOf(
Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)
remote_url=null, text_url=null, description=null, blurhash=null, meta = null)
),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),

View File

@ -49,7 +49,7 @@ private val REQUIRED_PERMISSIONS = arrayOf(
class PhotoEditActivity : BaseActivity() {
private var saving: Boolean = false
var saving: Boolean = false
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
private val BRIGHTNESS_START = 0
private val SATURATION_START = 1.0f

View File

@ -6,7 +6,6 @@ import android.view.View
import androidx.lifecycle.lifecycleScope
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.ActivityPostBinding
import com.h.pixeldroid.utils.api.objects.DiscoverPost
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.api.objects.Status.Companion.DISCOVER_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.DOMAIN_TAG
@ -30,7 +29,6 @@ class PostActivity : BaseActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val status = intent.getSerializableExtra(POST_TAG) as Status?
val discoverPost: DiscoverPost? = intent.getSerializableExtra(DISCOVER_TAG) as DiscoverPost?
val user = db.userDao().getActiveUser()
@ -41,12 +39,7 @@ class PostActivity : BaseActivity() {
val arguments = Bundle()
arguments.putString(DOMAIN_TAG, domain)
if (discoverPost != null) {
binding.postProgressBar.visibility = View.VISIBLE
getDiscoverPost(arguments, discoverPost)
} else {
initializeFragment(arguments, status)
}
initializeFragment(arguments, status)
}
override fun onSupportNavigateUp(): Boolean {
@ -54,25 +47,6 @@ class PostActivity : BaseActivity() {
return true
}
private fun getDiscoverPost(
arguments: Bundle,
discoverPost: DiscoverPost
) {
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
val id = discoverPost.url?.substringAfterLast('/') ?: ""
lifecycleScope.launchWhenCreated {
try {
val status = api.getStatus("Bearer $accessToken", id)
binding.postProgressBar.visibility = View.GONE
initializeFragment(arguments, status)
} catch (exception: IOException) {
//TODO show error message
Log.e("PostActivity:", exception.toString())
} catch (exception: HttpException) {
}
}
}
private fun initializeFragment(arguments: Bundle, status: Status?){
supportActionBar?.title = getString(R.string.post_title).format(status!!.account?.getDisplayName())
arguments.putSerializable(POST_TAG, status)

View File

@ -11,6 +11,7 @@ import com.h.pixeldroid.utils.api.objects.Status.Companion.DOMAIN_TAG
import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.utils.BaseFragment
import com.h.pixeldroid.utils.bindingLifecycleAware
import com.h.pixeldroid.utils.displayDimensionsInPx
class PostFragment : BaseFragment() {
@ -43,7 +44,9 @@ class PostFragment : BaseFragment() {
val holder = StatusViewHolder(binding)
holder.bind(currentStatus, api, db, lifecycleScope)
holder.bind(currentStatus, api, db, lifecycleScope, requireContext().displayDimensionsInPx())
}
}

View File

@ -3,9 +3,7 @@ package com.h.pixeldroid.posts
import android.Manifest
import android.app.AlertDialog
import android.content.Intent
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.text.method.LinkMovementMethod
import android.util.Log
@ -24,11 +22,13 @@ import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.AlbumImageViewBinding
import com.h.pixeldroid.databinding.CommentBinding
import com.h.pixeldroid.databinding.PostFragmentBinding
import com.h.pixeldroid.utils.BlurHashDecoder
import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.Attachment
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.db.AppDatabase
import com.h.pixeldroid.utils.displayDimensionsInPx
import com.karumi.dexter.Dexter
import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse
@ -36,6 +36,7 @@ import com.karumi.dexter.listener.single.BasePermissionListener
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
import kotlin.math.roundToInt
/**
@ -45,19 +46,30 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
private var status: Status? = null
fun bind(status: Status?, pixelfedAPI: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope) {
fun bind(status: Status?, pixelfedAPI: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair<Int, Int>) {
this.itemView.visibility = View.VISIBLE
this.status = status
val metrics = itemView.context.resources.displayMetrics
//Limit the height of the different images
binding.postPicture.maxHeight = metrics.heightPixels * 3/4
val maxImageRatio: Float = status?.media_attachments?.map {
if (it.meta?.original?.width == null || it.meta.original.height == null) {
1f
} else {
it.meta.original.width.toFloat() / it.meta.original.height.toFloat()
}
}?.maxOrNull() ?: 1f
val (displayWidth, displayHeight) = displayDimensionsInPx
val height = if (displayWidth / maxImageRatio > displayHeight * 3/4f) {
binding.postPicture.layoutParams.width = ((displayHeight * 3 / 4f) * maxImageRatio).roundToInt()
displayHeight * 3 / 4f
} else displayWidth / maxImageRatio
binding.postPicture.layoutParams.height = height.toInt()
//Setup the post layout
val picRequest = Glide.with(itemView)
.asDrawable().fitCenter()
.placeholder(ColorDrawable(Color.GRAY))
val user = db.userDao().getActiveUser()!!
@ -139,7 +151,17 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
if(status?.media_attachments?.size == 1) {
request.load(status?.getPostUrl()).into(binding.postPicture)
request.placeholder(
status?.media_attachments?.get(0).let {
it?.blurhash?.let { hash ->
BlurHashDecoder.blurHashBitmap(binding.root.resources,
hash,
it.meta?.original?.width,
it.meta?.original?.height
)
}
}
).load(status?.getPostUrl()).into(binding.postPicture)
val imgDescription = status?.media_attachments?.get(0)?.description.orEmpty().ifEmpty { binding.root.context.getString(
R.string.no_description) }
binding.postPicture.contentDescription = imgDescription
@ -687,7 +709,15 @@ class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) :
override fun getItemCount() = media_attachments.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
Glide.with(holder.binding.root)
.asDrawable().fitCenter().placeholder(ColorDrawable(Color.GRAY))
.asDrawable().fitCenter().placeholder(
media_attachments[position].blurhash?.let {
BlurHashDecoder.blurHashBitmap(
holder.binding.root.resources,
it,
media_attachments[position].meta?.original?.width,
media_attachments[position].meta?.original?.height)
}
)
.load(media_attachments[position].url).into(holder.image)
val description = media_attachments[position].description

View File

@ -19,6 +19,7 @@ import com.h.pixeldroid.posts.feeds.cachedFeeds.CachedFeedFragment
import com.h.pixeldroid.posts.feeds.cachedFeeds.ViewModelFactory
import com.h.pixeldroid.utils.api.objects.FeedContentDatabase
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.displayDimensionsInPx
/**
@ -35,7 +36,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = PostsAdapter()
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
@Suppress("UNCHECKED_CAST")
if (requireArguments().get("home") as Boolean){
@ -67,7 +68,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
return view
}
inner class PostsAdapter : PagingDataAdapter<T, RecyclerView.ViewHolder>(
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<T, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.id == newItem.id
@ -89,7 +90,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position) as Status
uiModel.let {
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope)
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope, displayDimensionsInPx)
}
}
}

View File

@ -15,6 +15,7 @@ import com.h.pixeldroid.posts.StatusViewHolder
import com.h.pixeldroid.posts.feeds.uncachedFeeds.*
import com.h.pixeldroid.utils.api.objects.Results
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.utils.displayDimensionsInPx
/**
* Fragment to show a list of [Status]es, as a result of a search.
@ -25,7 +26,7 @@ class SearchPostsFragment : UncachedFeedFragment<Status>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = PostsAdapter()
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
query = arguments?.getSerializable("searchFeed") as String
@ -57,7 +58,7 @@ class SearchPostsFragment : UncachedFeedFragment<Status>() {
return view
}
inner class PostsAdapter : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<Status>() {
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean {
return oldItem.id == newItem.id
@ -79,7 +80,7 @@ class SearchPostsFragment : UncachedFeedFragment<Status>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position) as Status
uiModel.let {
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope)
(holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope, displayDimensionsInPx)
}
}
}

View File

@ -4,25 +4,17 @@ import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.h.pixeldroid.R
import com.h.pixeldroid.databinding.FragmentSearchBinding
import com.h.pixeldroid.databinding.PostFragmentBinding
import com.h.pixeldroid.profile.ProfilePostViewHolder
import com.h.pixeldroid.utils.api.PixelfedAPI
import com.h.pixeldroid.utils.api.objects.DiscoverPost
import com.h.pixeldroid.utils.api.objects.DiscoverPosts
import com.h.pixeldroid.utils.api.objects.Status
import com.h.pixeldroid.posts.PostActivity
import com.h.pixeldroid.utils.BaseFragment
@ -34,10 +26,7 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.color
import com.mikepenz.iconics.utils.paddingDp
import com.mikepenz.iconics.utils.sizeDp
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException
/**
@ -122,12 +111,12 @@ class SearchDiscoverFragment : BaseFragment() {
}
/**
* [RecyclerView.Adapter] that can display a list of [DiscoverPost]s
* [RecyclerView.Adapter] that can display a list of [Status]s' thumbnails for the discover view
*/
class DiscoverRecyclerViewAdapter: RecyclerView.Adapter<ProfilePostViewHolder>() {
private val posts: ArrayList<DiscoverPost> = ArrayList()
private val posts: ArrayList<Status> = ArrayList()
fun addPosts(newPosts : List<DiscoverPost>) {
fun addPosts(newPosts : List<Status>) {
posts.clear()
posts.addAll(newPosts)
notifyDataSetChanged()
@ -141,15 +130,15 @@ class SearchDiscoverFragment : BaseFragment() {
override fun onBindViewHolder(holder: ProfilePostViewHolder, position: Int) {
val post = posts[position]
if(post.type?.contains("album") == true) {
if(post.media_attachments?.size ?: 0 > 1) {
holder.albumIcon.visibility = View.VISIBLE
} else {
holder.albumIcon.visibility = View.GONE
}
ImageConverter.setSquareImageFromURL(holder.postView, post.thumb, holder.postPreview)
ImageConverter.setSquareImageFromURL(holder.postView, post.media_attachments?.firstOrNull()?.preview_url, holder.postPreview, post.media_attachments?.firstOrNull()?.blurhash)
holder.postPreview.setOnClickListener {
val intent = Intent(holder.postView.context, PostActivity::class.java)
intent.putExtra(Status.DISCOVER_TAG, post)
intent.putExtra(Status.POST_TAG, post)
holder.postView.context.startActivity(intent)
}
}

View File

@ -0,0 +1,145 @@
package com.h.pixeldroid.utils
/**
* Blurhash implementation from blurhash project:
* https://github.com/woltapp/blurhash
* Minor modifications by charlag, for the Tusky project
* https://github.com/tuskyapp/Tusky/
*/
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import com.h.pixeldroid.utils.api.objects.Attachment
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.withSign
object BlurHashDecoder {
fun blurHashBitmap(resources: Resources, blurHash: String, width: Int?, height: Int?): BitmapDrawable {
val ratioOr0 = (width?.toFloat() ?: 1f) / (height?.toFloat() ?: 1f)
val ratio = if (ratioOr0 == 0f) 1f else ratioOr0
return BitmapDrawable(resources,
decode(blurHash,
(32f * ratio).toInt().coerceAtLeast(32),
(32f / ratio).toInt().coerceAtLeast(32))
)
}
fun decode(blurHash: String?, width: Int?, height: Int?, punch: Float = 1f): Bitmap? {
if (blurHash == null || width == null || height == null || blurHash.length < 6) {
return null
}
require(width > 0) { "Width must be greater than zero" }
require(height > 0) { "height must be greater than zero" }
val numCompEnc = decode83(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
return null
}
val maxAcEnc = decode83(blurHash, 1, 2)
val maxAc = (maxAcEnc + 1) / 166f
val colors = Array(numCompX * numCompY) { i ->
if (i == 0) {
val colorEnc = decode83(blurHash, 2, 6)
decodeDc(colorEnc)
} else {
val from = 4 + i * 2
val colorEnc = decode83(blurHash, from, from + 2)
decodeAc(colorEnc, maxAc * punch)
}
}
return composeBitmap(width, height, numCompX, numCompY, colors)
}
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
var result = 0
for (i in from until to) {
val index = charMap[str[i]] ?: -1
if (index != -1) {
result = result * 83 + index
}
}
return result
}
private fun decodeDc(colorEnc: Int): FloatArray {
val r = colorEnc shr 16
val g = (colorEnc shr 8) and 255
val b = colorEnc and 255
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
}
private fun srgbToLinear(colorEnc: Int): Float {
val v = colorEnc / 255f
return if (v <= 0.04045f) {
(v / 12.92f)
} else {
((v + 0.055f) / 1.055f).pow(2.4f)
}
}
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
val r = value / (19 * 19)
val g = (value / 19) % 19
val b = value % 19
return floatArrayOf(
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
signedPow2((b - 9) / 9.0f) * maxAc
)
}
private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
private fun composeBitmap(
width: Int, height: Int,
numCompX: Int, numCompY: Int,
colors: Array<FloatArray>
): Bitmap {
val imageArray = IntArray(width * height)
for (y in 0 until height) {
for (x in 0 until width) {
var r = 0f
var g = 0f
var b = 0f
for (j in 0 until numCompY) {
for (i in 0 until numCompX) {
val basis = (cos(PI * x * i / width) * cos(PI * y * j / height)).toFloat()
val color = colors[j * numCompX + i]
r += color[0] * basis
g += color[1] * basis
b += color[2] * basis
}
}
imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))
}
}
return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
}
private fun linearToSrgb(value: Float): Int {
val v = value.coerceIn(0f, 1f)
return if (v <= 0.0031308f) {
(v * 12.92f * 255f + 0.5f).toInt()
} else {
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
}
}
private val charMap = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
)
.mapIndexed { i, c -> c to i }
.toMap()
}

View File

@ -69,8 +69,10 @@ class ImageConverter {
* @param url, the url of the image that will be loaded
* @param image, the imageView into which we will load the image
*/
fun setSquareImageFromURL(view : View, url : String?, image : ImageView) {
Glide.with(view).load(url).apply(RequestOptions().centerCrop()).into(image)
fun setSquareImageFromURL(view : View, url : String?, image : ImageView, blurhash: String? = null) {
Glide.with(view).load(url).placeholder(
blurhash?.let { BlurHashDecoder.blurHashBitmap(view.resources, it, 32,32) }
).apply(RequestOptions().centerCrop()).into(image)
}

View File

@ -8,6 +8,8 @@ import android.content.res.Resources
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.util.DisplayMetrics
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.Fragment
@ -38,6 +40,20 @@ fun validDomain(domain: String?): Boolean {
return true
}
fun Context.displayDimensionsInPx(): Pair<Int, Int> {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Pair(windowManager.currentWindowMetrics.bounds.width(), windowManager.currentWindowMetrics.bounds.height())
} else {
val metrics = DisplayMetrics()
@Suppress("DEPRECATION")
windowManager.defaultDisplay.getMetrics(metrics)
Pair(metrics.widthPixels, metrics.heightPixels)
}
}
fun normalizeDomain(domain: String): String {
return "https://" + domain
.replace("http://", "")

View File

@ -11,11 +11,31 @@ data class Attachment(
//Optional attributes
val remote_url: String? = null, //URL
val text_url: String? = null, //URL
//TODO meta
val meta: Meta?,
val description: String? = null,
val blurhash: String? = null
) : Serializable {
enum class AttachmentType {
unknown, image, gifv, video, audio
}
data class Meta (
val focus: Focus?,
val original: Image?
) : Serializable
{
data class Focus(
val x: Double?,
val y: Double?
) : Serializable
data class Image(
val width: Int?,
val height: Int?,
val size: String?,
val aspect: Double?
) : Serializable
}
}

View File

@ -1,13 +0,0 @@
package com.h.pixeldroid.utils.api.objects
import java.io.Serializable
/*
NOT DOCUMENTED, USE WITH CAUTION
*/
data class DiscoverPost(
val type: String?, //This is probably an enum, with these values: https://github.com/pixelfed/pixelfed/blob/700c7805cecc364b68b9cfe20df00608e0f6c465/app/Status.php#L31
val url: String?, //URL to post
val thumb: String? //URL to thumbnail
) : Serializable

View File

@ -4,5 +4,5 @@ import java.io.Serializable
data class DiscoverPosts(
//Required attributes
val posts: List<DiscoverPost>
val posts: List<Status>
) : Serializable

View File

@ -6,17 +6,6 @@
android:layout_height="match_parent"
tools:context=".posts.PostActivity">
<ProgressBar
android:id="@+id/postProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">

View File

@ -76,8 +76,7 @@
<ImageView
android:id="@+id/postPicture"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:layout_height="200dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View File

@ -31,7 +31,7 @@ class APIUnitTest {
visibility=Status.Visibility.public, sensitive=false, spoiler_text="",
media_attachments= listOf(Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)),
remote_url=null, text_url=null, description=null, blurhash=null, meta = null)),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),
emojis= emptyList(), reblogs_count=0, favourites_count=0, replies_count=0, url="https://pixelfed.de/p/Miike/140364967936397312",

View File

@ -20,7 +20,7 @@ class PostUnitTest {
media_attachments= listOf(
Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg",
preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg",
remote_url=null, text_url=null, description=null, blurhash=null)
remote_url=null, text_url=null, description=null, blurhash=null, meta = null)
),
application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(),
tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)),