1
0
mirror of https://gitlab.shinice.net/pixeldroid/PixelDroid synced 2025-02-07 15:18:46 +01:00

More progress on stories :)

This commit is contained in:
Matthieu 2023-05-13 15:12:51 +02:00
parent f60889ea14
commit 73193abd95
12 changed files with 8349 additions and 106 deletions

View File

@ -138,7 +138,7 @@ dependencies {
*/
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
@ -186,7 +186,7 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.google.android.material:material:1.9.0'
//Dagger (dependency injection)
implementation 'com.google.dagger:dagger-android:2.45'

View File

@ -11,6 +11,7 @@ import android.view.View
import android.widget.TextView
import androidx.core.text.toSpanned
import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account.Companion.openAccountFromId
@ -106,7 +107,7 @@ fun parseHTMLText(
override fun onClick(widget: View) {
// Retrieve the account for the given profile
lifecycleScope.launchWhenCreated {
lifecycleScope.launch {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
openAccountFromId(accountId, api, context)
}

View File

@ -1,5 +1,6 @@
package org.pixeldroid.app.searchDiscover
import android.annotation.SuppressLint
import android.app.SearchManager
import android.content.Context
import android.content.Intent
@ -8,25 +9,38 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.google.android.material.carousel.CarouselLayoutManager
import kotlinx.coroutines.launch
import org.pixeldroid.app.databinding.FragmentSearchBinding
import org.pixeldroid.app.databinding.StoryCarouselBinding
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TRENDING_TAG
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TrendingType
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.stories.StoriesActivity
import org.pixeldroid.app.stories.StoriesActivity.Companion.STORY_CAROUSEL
import org.pixeldroid.app.stories.StoriesActivity.Companion.STORY_CAROUSEL_USER_ID
import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
import org.pixeldroid.app.utils.api.objects.StoryCarousel
import org.pixeldroid.app.utils.bindingLifecycleAware
/**
* This fragment lets you search and use Pixelfed's Discover feature
*/
class SearchDiscoverFragment : BaseFragment() {
private lateinit var api: PixelfedAPI
var binding: FragmentSearchBinding by bindingLifecycleAware()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View {
binding = FragmentSearchBinding.inflate(inflater, container, false)
@ -37,6 +51,13 @@ class SearchDiscoverFragment : BaseFragment() {
isSubmitButtonEnabled = true
}
val adapter = StoriesListAdapter(::onClickStory)
binding.recyclerView2.adapter = adapter
loadStories(adapter)
binding.recyclerView2.layoutManager = CarouselLayoutManager()
return binding.root
}
@ -56,4 +77,69 @@ class SearchDiscoverFragment : BaseFragment() {
intent.putExtra(TRENDING_TAG, type)
ContextCompat.startActivity(binding.root.context, intent, null)
}
private fun onClickStory(carousel: StoryCarousel, userId: String){
val intent = Intent(requireContext(), StoriesActivity::class.java)
intent.putExtra(STORY_CAROUSEL, carousel)
intent.putExtra(STORY_CAROUSEL_USER_ID, userId)
startActivity(intent)
}
private fun loadStories(adapter: StoriesListAdapter) {
lifecycleScope.launch {
try{
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val carousel = api.carousel()
adapter.initCarousel(carousel)
} catch (exception: Exception){
//TODO
}
}
}
}
class StoriesListAdapter(private val listener: (StoryCarousel, String) -> Unit): RecyclerView.Adapter<StoriesListAdapter.ViewHolder>() {
private var storyCarousel: StoryCarousel? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = StoryCarouselBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
storyCarousel?.nodes?.get(position)?.let { holder.bindItem(it) }
holder.itemView.setOnClickListener {
storyCarousel?.let { carousel ->
storyCarousel?.nodes?.get(position)?.user?.id?.let { userId ->
listener(
carousel,
userId
)
}
}
}
}
override fun getItemCount(): Int {
return storyCarousel?.nodes?.size ?: 0
}
@SuppressLint("NotifyDataSetChanged")
fun initCarousel(carousel: StoryCarousel){
storyCarousel = carousel
notifyDataSetChanged()
}
class ViewHolder(var itemBinding: StoryCarouselBinding) :
RecyclerView.ViewHolder(itemBinding.root) {
fun bindItem(user: CarouselUserContainer) {
Glide.with(itemBinding.root).load(user.nodes?.firstOrNull()?.src).into(itemBinding.carouselImageView)
Glide.with(itemBinding.root).load(user.user?.avatar).circleCrop().into(itemBinding.storyAuthorProfilePicture)
itemBinding.username.text = user.user?.username ?: "" //TODO check which one to use here!
}
}
}

View File

@ -3,6 +3,9 @@ package org.pixeldroid.app.stories
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.view.MotionEvent
import android.view.View.OnClickListener
import android.view.View.OnTouchListener
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
@ -21,10 +24,18 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityStoriesBinding
import org.pixeldroid.app.posts.setTextViewFromISO8601
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.StoryCarousel
class StoriesActivity: BaseThemedWithoutBarActivity() {
companion object {
const val STORY_CAROUSEL = "LaunchStoryCarousel"
const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId"
}
private lateinit var binding: ActivityStoriesBinding
private lateinit var model: StoriesViewModel
@ -32,11 +43,14 @@ class StoriesActivity: BaseThemedWithoutBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as StoryCarousel
val userId = intent.getStringExtra(STORY_CAROUSEL_USER_ID)
binding = ActivityStoriesBinding.inflate(layoutInflater)
setContentView(binding.root)
val _model: StoriesViewModel by viewModels {
StoriesViewModelFactory(application)
StoriesViewModelFactory(application, carousel, userId)
}
model = _model
@ -48,7 +62,7 @@ class StoriesActivity: BaseThemedWithoutBarActivity() {
uiState.age?.let { setTextViewFromISO8601(it, binding.storyAge, false) }
if (uiState.errorMessage != null) {
binding.storyErrorText.text = uiState.errorMessage
binding.storyErrorText.setText(uiState.errorMessage)
binding.storyErrorCard.isVisible = true
} else binding.storyErrorCard.isVisible = false
@ -73,6 +87,9 @@ class StoriesActivity: BaseThemedWithoutBarActivity() {
binding.storyAuthor.text = uiState.username
binding.carouselProgress.text = getString(R.string.storyProgress)
.format(uiState.currentImage + 1, uiState.imageList.size)
uiState.imageList.getOrNull(uiState.currentImage)?.let {
Glide.with(binding.storyImage)
.load(it)
@ -91,7 +108,9 @@ class StoriesActivity: BaseThemedWithoutBarActivity() {
dataSource: DataSource?,
isFirstResource: Boolean,
): Boolean {
model.imageLoaded()
Glide.with(binding.storyImage)
.load(uiState.imageList.getOrNull(uiState.currentImage + 1))
.preload()
return false
}
})
@ -100,6 +119,19 @@ class StoriesActivity: BaseThemedWithoutBarActivity() {
}
}
}
//Pause when clicked on text field
binding.storyReplyField.editText?.setOnFocusChangeListener { view, isFocused ->
if (view.isInTouchMode && isFocused) {
view.performClick() // picks up first tap
}
}
binding.storyReplyField.editText?.setOnClickListener {
if (!model.uiState.value.paused) {
model.pause()
}
}
binding.storyReplyField.editText?.doAfterTextChanged {
it?.let { text ->
val string = text.toString()
@ -134,10 +166,60 @@ class StoriesActivity: BaseThemedWithoutBarActivity() {
//Set the button's appearance
it.isSelected = !it.isSelected
model.pause()
}
val authorOnClickListener = OnClickListener {
if (!model.uiState.value.paused) {
model.pause()
}
model.currentProfileId()?.let {
lifecycleScope.launch {
Account.openAccountFromId(
it,
apiHolder.api ?: apiHolder.setToCurrentUser(),
this@StoriesActivity
)
}
}
}
binding.storyAuthorProfilePicture.setOnClickListener(authorOnClickListener)
binding.storyAuthor.setOnClickListener(authorOnClickListener)
val onTouchListener = OnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> if (!model.uiState.value.paused) {
model.pause()
}
MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) {
v.performClick()
return@OnTouchListener false
} else model.pause()
}
binding.storyImage.setOnClickListener {
model.pause()
true
}
binding.viewMiddle.setOnTouchListener{ v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> model.pause()
MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) {
v.performClick()
return@setOnTouchListener false
} else model.pause()
}
true
}
binding.viewLeft.setOnTouchListener(onTouchListener)
binding.viewRight.setOnTouchListener(onTouchListener)
//TODO implement hold to pause
binding.viewRight.setOnClickListener {
model.goToNext()
}
binding.viewLeft.setOnClickListener {
model.goToPrevious()
}
}
}

View File

@ -3,13 +3,12 @@ package org.pixeldroid.app.stories
import android.app.Application
import android.os.CountDownTimer
import android.text.Editable
import android.util.Log
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@ -29,40 +28,45 @@ data class StoriesUiState(
val imageList: List<String> = emptyList(),
val durationList: List<Int> = emptyList(),
val paused: Boolean = false,
val errorMessage: String? = null,
val snackBar: String? = null,
@StringRes
val errorMessage: Int? = null,
@StringRes
val snackBar: Int? = null,
val reply: String = ""
)
class StoriesViewModel(
application: Application,
val carousel: StoryCarousel,
userId: String?
) : AndroidViewModel(application) {
@Inject
lateinit var apiHolder: PixelfedAPIHolder
private val _uiState: MutableStateFlow<StoriesUiState> = MutableStateFlow(StoriesUiState())
private var currentAccount = carousel.nodes?.firstOrNull { it?.user?.id == userId }
private val _uiState: MutableStateFlow<StoriesUiState> = MutableStateFlow(
newUiStateFromCurrentAccount()
)
val uiState: StateFlow<StoriesUiState> = _uiState
var carousel: StoryCarousel? = null
val count = MutableLiveData<Long>()
val count = MutableLiveData<Float>()
private var timer: CountDownTimer? = null
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
loadStories()
startTimerForCurrent()
}
private fun setTimer(timerLength: Long) {
private fun setTimer(timerLength: Float) {
count.value = timerLength
timer = object: CountDownTimer(timerLength * 1000, 500){
timer = object: CountDownTimer((timerLength * 1000).toLong(), 100){
override fun onTick(millisUntilFinished: Long) {
count.value = millisUntilFinished / 1000
Log.e("Timer second", "${count.value}")
count.value = millisUntilFinished.toFloat() / 1000
}
override fun onFinish() {
@ -71,63 +75,63 @@ class StoriesViewModel(
}
}
private fun goToNext(){
_uiState.update { currentUiState ->
currentUiState.copy(
currentImage = currentUiState.currentImage + 1,
//TODO don't just take the first here, choose from activity input somehow?
age = carousel?.nodes?.firstOrNull()?.nodes?.getOrNull(currentUiState.currentImage + 1)?.created_at
)
private fun newUiStateFromCurrentAccount(): StoriesUiState = StoriesUiState(
profilePicture = currentAccount?.user?.avatar,
age = currentAccount?.nodes?.getOrNull(0)?.created_at,
username = currentAccount?.user?.username, //TODO check if not username_acct, think about falling back on other option?
errorMessage = null,
currentImage = 0,
imageList = currentAccount?.nodes?.mapNotNull { it?.src } ?: emptyList(),
durationList = currentAccount?.nodes?.mapNotNull { it?.duration } ?: emptyList()
)
private fun goTo(index: Int){
if((0 until uiState.value.imageList.size).contains(index)) {
_uiState.update { currentUiState ->
currentUiState.copy(
currentImage = index,
age = currentAccount?.nodes?.getOrNull(index)?.created_at,
paused = false
)
}
} else {
val currentUserId = currentAccount?.user?.id
val currentAccountIndex = carousel.nodes?.indexOfFirst { it?.user?.id == currentUserId } ?: return
currentAccount = when (index) {
uiState.value.imageList.size -> {
// Go to next user
if(currentAccountIndex + 1 >= carousel.nodes.size) return
carousel.nodes.getOrNull(currentAccountIndex + 1)
}
-1 -> {
// Go to previous user
if(currentAccountIndex <= 0) return
carousel.nodes.getOrNull(currentAccountIndex - 1)
}
else -> return // Do nothing, given index does not make sense
}
_uiState.update { newUiStateFromCurrentAccount() }
}
//TODO when done with viewing all stories, close activity and move to profile (?)
timer?.cancel()
startTimerForCurrent()
}
private fun loadStories() {
viewModelScope.launch {
try{
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
carousel = api.carousel()
fun goToNext() = goTo(uiState.value.currentImage + 1)
//TODO don't just take the first here, choose from activity input somehow?
val chosenAccount = carousel?.nodes?.firstOrNull()
_uiState.update { currentUiState ->
currentUiState.copy(
profilePicture = chosenAccount?.user?.avatar,
age = chosenAccount?.nodes?.getOrNull(0)?.created_at,
username = chosenAccount?.user?.username, //TODO check if not username_acct, think about falling back on other option?
errorMessage = null,
currentImage = 0,
imageList = chosenAccount?.nodes?.mapNotNull { it?.src } ?: emptyList(),
durationList = chosenAccount?.nodes?.mapNotNull { it?.duration } ?: emptyList()
)
}
startTimerForCurrent()
} catch (exception: Exception){
_uiState.update { currentUiState ->
currentUiState.copy(errorMessage = "Something went wrong fetching the carousel")
}
}
}
}
fun goToPrevious() = goTo(uiState.value.currentImage - 1)
private fun startTimerForCurrent(){
uiState.value.let {
it.durationList.getOrNull(it.currentImage)?.toLong()?.let { time ->
setTimer(time)
setTimer(time.toFloat())
timer?.start()
}
}
}
fun imageLoaded() {/*
_uiState.update { currentUiState ->
currentUiState.copy(currentImage = currentUiState.currentImage + 1)
}*/
}
fun pause() {
if(_uiState.value.paused){
timer?.start()
@ -144,16 +148,15 @@ class StoriesViewModel(
viewModelScope.launch {
try {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
//TODO don't just take the first here, choose from activity input somehow?
val id = carousel?.nodes?.firstOrNull()?.nodes?.getOrNull(uiState.value.currentImage)?.id
val id = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)?.id
id?.let { api.storyComment(it, text.toString()) }
_uiState.update { currentUiState ->
currentUiState.copy(snackBar = "Sent reply")
currentUiState.copy(snackBar = R.string.sent_reply_story)
}
} catch (exception: Exception){
_uiState.update { currentUiState ->
currentUiState.copy(errorMessage = "Something went wrong sending reply")
currentUiState.copy(errorMessage = R.string.story_reply_error)
}
}
@ -177,10 +180,17 @@ class StoriesViewModel(
currentUiState.copy(snackBar = null)
}
}
fun currentProfileId(): String? = currentAccount?.user?.id
}
class StoriesViewModelFactory(val application: Application) : ViewModelProvider.Factory {
class StoriesViewModelFactory(
val application: Application,
val carousel: StoryCarousel,
val userId: String?
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java).newInstance(application)
return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java).newInstance(application, carousel, userId)
}
}

View File

@ -57,11 +57,13 @@ data class Account(
suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context) {
val account = try {
api.getAccount(id)
} catch (exception: IOException) {
Log.e("GET ACCOUNT ERROR", exception.toString())
return
} catch (exception: HttpException) {
Log.e("ERROR CODE", exception.code().toString())
} catch (exception: Exception) {
val toLog = if (exception is HttpException) {
exception.code().toString()
} else {
exception.toString()
}
Log.e("GET ACCOUNT ERROR", toLog)
return
}
//Open the account page in a separate activity

View File

@ -1,11 +1,12 @@
package org.pixeldroid.app.utils.api.objects
import java.io.Serializable
import java.time.Instant
data class StoryCarousel(
val self: CarouselUserContainer?,
val nodes: List<CarouselUserContainer?>?
)
): Serializable
data class CarouselUser(
val id: String?,
@ -14,7 +15,7 @@ data class CarouselUser(
val avatar: String?, // URL to account avatar
val local: Boolean?, // Is this story from the local instance?
val is_author: Boolean?, // Is this me? (seems redundant with id)
)
): Serializable
/**
* Container with a description of the [user] and a list of stories ([nodes])
@ -22,7 +23,7 @@ data class CarouselUser(
data class CarouselUserContainer(
val user: CarouselUser?,
val nodes: List<Story?>?,
)
): Serializable
data class Story(
val id: String?,
@ -32,4 +33,4 @@ data class Story(
val duration: Int?, //Time in seconds that the Story should be shown
val seen: Boolean?, //Indication of whether this story has been seen. Set to true using carouselSeen
val created_at: Instant?, //ISO 8601 Datetime
)
): Serializable

View File

@ -54,11 +54,13 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:contentDescription="@string/story_image"
tools:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBarStory"
app:layout_constraintVertical_bias="1.0"
tools:scaleType="centerCrop"
tools:srcCompat="@tools:sample/backgrounds/scenic[10]" />
<ImageButton
@ -79,6 +81,7 @@
android:id="@+id/progressBarStory"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:progress="0"
tools:progress="56"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
@ -133,5 +136,45 @@
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/carouselProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
app:layout_constraintBottom_toBottomOf="@+id/pause"
app:layout_constraintEnd_toStartOf="@+id/pause"
app:layout_constraintTop_toTopOf="@+id/pause"
tools:text="2/3" />
<View
android:id="@+id/viewRight"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/viewMiddle"
app:layout_constraintBottom_toTopOf="@id/storyReplyField"
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" />
<View
android:id="@+id/viewMiddle"
android:layout_width="80dp"
android:layout_height="0dp"
app:layout_constraintEnd_toStartOf="@id/viewRight"
app:layout_constraintStart_toEndOf="@id/viewLeft"
app:layout_constraintBottom_toTopOf="@id/storyReplyField"
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" />
<View
android:id="@+id/viewLeft"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/viewMiddle"
app:layout_constraintBottom_toTopOf="@id/storyReplyField"
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,9 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.SearchView
android:id="@+id/search"
@ -22,14 +25,14 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search">
app:layout_constraintTop_toBottomOf="@+id/recyclerView2">
<com.google.android.material.card.MaterialCardView
android:id="@+id/trendingCardView"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintBottom_toTopOf="@+id/hashtagsCardView"
app:layout_constraintEnd_toEndOf="parent"
@ -45,28 +48,28 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:text="@string/trending_posts"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:drawablePadding="4dp"
android:textColor="?attr/colorOnSecondaryContainer"
app:drawableLeftCompat="@drawable/baseline_auto_graph_24" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/daily_trending"
android:textColor="?attr/colorOnSecondaryContainer"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:layout_marginTop="8dp" />
android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/hashtagsCardView"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintBottom_toTopOf="@id/accountsCardView"
app:layout_constraintEnd_toEndOf="parent"
@ -82,33 +85,33 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:text="@string/trending_hashtags"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:drawablePadding="4dp"
android:textColor="?attr/colorOnSecondaryContainer"
app:drawableStartCompat="@drawable/baseline_tag" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/explore_hashtags"
android:textColor="?attr/colorOnSecondaryContainer"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:layout_marginTop="8dp" />
android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/accountsCardView"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintTop_toBottomOf="@id/hashtagsCardView"
app:layout_constraintBottom_toTopOf="@id/discoverCardView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/discoverCardView">
app:layout_constraintTop_toBottomOf="@id/hashtagsCardView">
<LinearLayout
android:layout_width="match_parent"
@ -119,33 +122,33 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:text="@string/popular_accounts"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:drawablePadding="4dp"
android:textColor="?attr/colorOnSecondaryContainer"
app:drawableStartCompat="@drawable/baseline_person_add" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/explore_accounts"
android:textColor="?attr/colorOnSecondaryContainer"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:layout_marginTop="8dp" />
android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/discoverCardView"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintTop_toBottomOf="@id/accountsCardView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
app:layout_constraintTop_toBottomOf="@id/accountsCardView">
<LinearLayout
android:layout_width="match_parent"
@ -156,22 +159,34 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:text="@string/discover"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:drawablePadding="4dp"
android:textColor="?attr/colorOnSecondaryContainer"
app:drawableStartCompat="@drawable/explore_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/explore_posts"
android:textColor="?attr/colorOnSecondaryContainer"
android:textAppearance="?attr/textAppearanceTitleSmall"
android:layout_marginTop="8dp" />
android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView2"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="horizontal"
android:clipChildren="false"
android:clipToPadding="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.carousel.MaskableFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/carousel_item_container"
android:layout_width="120dp"
android:layout_height="match_parent"
tools:layout_height="240dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:foreground="?attr/selectableItemBackground"
app:shapeAppearance="?attr/shapeAppearanceCornerExtraLarge">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/carousel_image_view"
android:contentDescription="@string/story_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
<ImageView
android:id="@+id/storyAuthorProfilePicture"
android:contentDescription="@string/profile_picture"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginBottom="6dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toTopOf="@id/username"
app:layout_constraintStart_toStartOf="@id/username"
tools:srcCompat="@tools:sample/avatars[3]" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="pixeldroid" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.carousel.MaskableFrameLayout>

View File

@ -332,4 +332,8 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="summary_always_show_nsfw">NSFW/CW posts will not be blurred, and will be shown by default.</string>
<string name="story_image">Story image</string>
<string name="replyToStory">Reply to %1$s</string>
<string name="storyProgress">%1$s / %2$s</string>
<string name="story_reply_error">Something went wrong sending reply</string>
<string name="error_fetch_story">Something went wrong fetching the carousel</string>
<string name="sent_reply_story">Sent reply</string>
</resources>

File diff suppressed because it is too large Load Diff