Add Self view for stories

This commit is contained in:
Matthieu 2023-12-21 14:03:36 +01:00
parent 0290e6f8d5
commit bb3c9afb13
8 changed files with 208 additions and 85 deletions

View File

@ -1,7 +1,6 @@
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
@ -26,6 +25,7 @@ import org.pixeldroid.app.databinding.ActivityStoriesBinding
import org.pixeldroid.app.posts.setTextViewFromISO8601
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Story
import org.pixeldroid.app.utils.api.objects.StoryCarousel
@ -33,6 +33,7 @@ class StoriesActivity: BaseActivity() {
companion object {
const val STORY_CAROUSEL = "LaunchStoryCarousel"
const val STORY_CAROUSEL_SELF = "LaunchStoryCarouselSelf"
const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId"
}
@ -49,14 +50,15 @@ class StoriesActivity: BaseActivity() {
super.onCreate(savedInstanceState)
val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as StoryCarousel
val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as? StoryCarousel
val userId = intent.getStringExtra(STORY_CAROUSEL_USER_ID)
val selfCarousel: Array<Story>? = intent.getSerializableExtra(STORY_CAROUSEL_SELF) as? Array<Story>
binding = ActivityStoriesBinding.inflate(layoutInflater)
setContentView(binding.root)
val _model: StoriesViewModel by viewModels {
StoriesViewModelFactory(application, carousel, userId)
StoriesViewModelFactory(application, carousel, userId, selfCarousel?.asList())
}
model = _model

View File

@ -4,21 +4,21 @@ import android.app.Application
import android.os.CountDownTimer
import android.text.Editable
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
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.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
import org.pixeldroid.app.utils.api.objects.Story
import org.pixeldroid.app.utils.api.objects.StoryCarousel
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import java.time.Instant
import javax.inject.Inject
@ -40,20 +40,21 @@ data class StoriesUiState(
class StoriesViewModel(
application: Application,
val carousel: StoryCarousel,
userId: String?
val carousel: StoryCarousel?,
userId: String?,
val selfCarousel: List<Story>?
) : AndroidViewModel(application) {
@Inject
lateinit var apiHolder: PixelfedAPIHolder
@Inject
lateinit var db: AppDatabase
private var currentAccount = carousel.nodes?.firstOrNull { it?.user?.id == userId }
private var currentAccount: CarouselUserContainer?
private val _uiState: MutableStateFlow<StoriesUiState> = MutableStateFlow(
newUiStateFromCurrentAccount()
)
private val _uiState: MutableStateFlow<StoriesUiState>
val uiState: StateFlow<StoriesUiState> = _uiState
val uiState: StateFlow<StoriesUiState>
val count = MutableLiveData<Float>()
@ -61,12 +62,20 @@ class StoriesViewModel(
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
currentAccount =
if (selfCarousel != null) {
db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel) }
} else carousel?.nodes?.firstOrNull { it?.user?.id == userId }
_uiState = MutableStateFlow(newUiStateFromCurrentAccount())
uiState = _uiState
startTimerForCurrent()
}
private fun setTimer(timerLength: Float) {
count.value = timerLength
timer = object: CountDownTimer((timerLength * 1000).toLong(), 100){
timer = object: CountDownTimer((timerLength * 1000).toLong(), 50){
override fun onTick(millisUntilFinished: Long) {
count.value = millisUntilFinished.toFloat() / 1000
@ -98,8 +107,9 @@ class StoriesViewModel(
)
}
} else {
if(selfCarousel != null) return
val currentUserId = currentAccount?.user?.id
val currentAccountIndex = carousel.nodes?.indexOfFirst { it?.user?.id == currentUserId } ?: return
val currentAccountIndex = carousel?.nodes?.indexOfFirst { it?.user?.id == currentUserId } ?: return
currentAccount = when (index) {
uiState.value.imageList.size -> {
// Go to next user
@ -209,10 +219,11 @@ class StoriesViewModel(
class StoriesViewModelFactory(
val application: Application,
val carousel: StoryCarousel,
val userId: String?
val carousel: StoryCarousel?,
val userId: String?,
val selfCarousel: List<Story>?
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java).newInstance(application, carousel, userId)
return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java, List::class.java).newInstance(application, carousel, userId, selfCarousel)
}
}

View File

@ -2,26 +2,33 @@ package org.pixeldroid.app.stories
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.StoryCarouselAddStoryBinding
import org.pixeldroid.app.databinding.StoryCarouselBinding
import org.pixeldroid.app.databinding.StoryCarouselItemBinding
import org.pixeldroid.app.databinding.StoryCarouselSelfBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
import org.pixeldroid.app.utils.api.objects.Story
import org.pixeldroid.app.utils.api.objects.StoryCarousel
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
/**
* Adapter to the show the a [RecyclerView] item for a [LoadState]
* Adapter that has either 1 or 0 items, to show stories widget or not
*/
class StoriesAdapter(val lifecycleScope: LifecycleCoroutineScope, val apiHolder: PixelfedAPIHolder) : RecyclerView.Adapter<StoryCarouselViewHolder>() {
var carousel: StoryCarousel? = null
@ -74,7 +81,8 @@ class StoriesAdapter(val lifecycleScope: LifecycleCoroutineScope, val apiHolder:
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val carousel = api.carousel()
if (carousel.nodes?.isEmpty() != true) {
// If there are stories from someone else or our stories to show, show them
if (carousel.nodes?.isEmpty() == false || carousel.self?.nodes?.isEmpty() == false) {
// Pass carousel to adapter
gotStories(carousel)
} else {
@ -113,8 +121,12 @@ class StoriesListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var storyCarousel: StoryCarousel? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if(viewType == R.layout.story_carousel_add_story){
val v = StoryCarouselAddStoryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return if(viewType == R.layout.story_carousel_self){
val v = StoryCarouselSelfBinding.inflate(LayoutInflater.from(parent.context), parent, false)
v.myStory.visibility =
if (storyCarousel?.self?.nodes?.isEmpty() == false) View.VISIBLE
else View.GONE
AddViewHolder(v)
}
else {
@ -124,7 +136,7 @@ class StoriesListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
}
override fun getItemViewType(position: Int): Int {
return if(position == 0) R.layout.story_carousel_add_story
return if(position == 0) R.layout.story_carousel_self
else R.layout.story_carousel_item
}
@ -133,21 +145,15 @@ class StoriesListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val carouselPosition = position - 1
storyCarousel?.nodes?.get(carouselPosition)?.let { (holder as ViewHolder).bindItem(it) }
holder.itemView.setOnClickListener {
storyCarousel?.let { carousel ->
storyCarousel?.nodes?.get(carouselPosition)?.user?.id?.let { userId ->
val intent = Intent(holder.itemView.context, StoriesActivity::class.java)
intent.putExtra(StoriesActivity.STORY_CAROUSEL, carousel)
intent.putExtra(StoriesActivity.STORY_CAROUSEL, storyCarousel)
intent.putExtra(StoriesActivity.STORY_CAROUSEL_USER_ID, userId)
holder.itemView.context.startActivity(intent)
}
}
}
} else {
holder.itemView.setOnClickListener {
val intent = Intent(holder.itemView.context, CameraActivity::class.java)
intent.putExtra(CameraFragment.CAMERA_ACTIVITY_STORY, true)
holder.itemView.context.startActivity(intent)
}
storyCarousel?.self?.nodes?.let { (holder as? AddViewHolder)?.bindItem(it.filterNotNull()) }
}
}
@ -162,7 +168,35 @@ class StoriesListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
notifyDataSetChanged()
}
class AddViewHolder(itemBinding: StoryCarouselAddStoryBinding) : RecyclerView.ViewHolder(itemBinding.root)
class AddViewHolder(private val itemBinding: StoryCarouselSelfBinding) : RecyclerView.ViewHolder(itemBinding.root) {
fun bindItem(nodes: List<Story>) {
itemBinding.addStory.setOnClickListener {
val intent = Intent(itemView.context, CameraActivity::class.java)
intent.putExtra(CameraFragment.CAMERA_ACTIVITY_STORY, true)
itemView.context.startActivity(intent)
}
itemBinding.myStory.setOnClickListener {
val intent = Intent(itemView.context, StoriesActivity::class.java)
intent.putExtra(StoriesActivity.STORY_CAROUSEL_SELF, nodes.toTypedArray())
itemView.context.startActivity(intent)
}
// Only show image on new Android versions, because the transformations need it and the
// text is not legible without the transformations
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Glide.with(itemBinding.root).load(nodes.firstOrNull()?.src).into(itemBinding.carouselImageView)
val value = 70 * 255 / 100
val darkFilterRenderEffect = PorterDuffColorFilter(Color.argb(value, 0, 0, 0), PorterDuff.Mode.SRC_ATOP)
val blurRenderEffect =
RenderEffect.createBlurEffect(
4f, 4f, Shader.TileMode.MIRROR
)
val combinedEffect = RenderEffect.createColorFilterEffect(darkFilterRenderEffect, blurRenderEffect)
itemBinding.carouselImageView.setRenderEffect(combinedEffect)
}
}
}
class ViewHolder(private val itemBinding: StoryCarouselItemBinding) :
RecyclerView.ViewHolder(itemBinding.root) {

View File

@ -1,5 +1,6 @@
package org.pixeldroid.app.utils.api.objects
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import java.io.Serializable
import java.time.Instant
@ -23,7 +24,13 @@ data class CarouselUser(
data class CarouselUserContainer(
val user: CarouselUser?,
val nodes: List<Story?>?,
): Serializable
): Serializable {
constructor(user: UserDatabaseEntity, nodes: List<Story?>?) : this(
CarouselUser(user.user_id, user.username, null, user.avatar_static,
local = true,
is_author = true
), nodes)
}
data class Story(
val id: String?,

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,-2zM12,14.5v-9l6,4.5 -6,4.5z"/>
</vector>

View File

@ -1,46 +0,0 @@
<?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_add_story"
android:layout_width="120dp"
android:layout_height="match_parent"
android:layout_marginStart="4dp"
tools:context="androidx.recyclerview.widget.RecyclerView"
android:layout_marginEnd="4dp"
android:background="?attr/colorSecondaryContainer"
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:layout_width="match_parent"
android:layout_height="40dp"
android:contentDescription="@string/story_image"
android:scaleType="fitCenter"
android:src="@drawable/collection_add"
app:layout_constraintBottom_toTopOf="@id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnSecondaryContainer" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_story"
android:textColor="?attr/colorOnSecondaryContainer"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/carousel_image_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.carousel.MaskableFrameLayout>

View File

@ -0,0 +1,102 @@
<?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_add_story"
android:layout_width="120dp"
android:layout_height="match_parent"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="?attr/colorStoryImage"
app:shapeAppearance="?attr/shapeAppearanceCornerExtraLarge"
tools:context="androidx.recyclerview.widget.RecyclerView">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/carousel_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/story_image"
android:scaleType="centerCrop"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/add_story"
android:foreground="?attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/my_story"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/carousel_add_story_icon"
android:layout_width="match_parent"
android:layout_height="40dp"
android:contentDescription="@string/add_story"
android:scaleType="fitCenter"
android:src="@drawable/collection_add"
app:layout_constraintBottom_toTopOf="@id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnStoryImage" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_story"
android:textColor="?attr/colorOnStoryImage"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/carousel_add_story_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/my_story"
android:foreground="?attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_story"
tools:visibility="visible">
<ImageView
android:id="@+id/my_story_icon"
android:layout_width="match_parent"
android:layout_height="40dp"
android:contentDescription="@string/my_story"
android:scaleType="fitCenter"
android:src="@drawable/story_play"
app:layout_constraintBottom_toTopOf="@id/my_story_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnStoryImage" />
<TextView
android:id="@+id/my_story_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/my_story"
android:textColor="?attr/colorOnStoryImage"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/my_story_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.carousel.MaskableFrameLayout>

View File

@ -338,4 +338,12 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="add_story">Add Story</string>
<string name="story_could_not_see">Error: could not mark story as seen</string>
<string name="story_pause">Start or pause the stories</string>
<string name="my_story">My story</string>
<!-- Type of new publication that is being created (as opposed to a traditional post, if this is chosen it would be a story) -->
<string name="type_story">Story</string>
<!-- Type of new publication that is being created (as opposed to a story, if this is chosen it would be a traditional post) -->
<string name="type_post">Post</string>
<string name="continue_post_creation">Continue</string>
<string name="extraneous_pictures_stories">Pictures after the first were removed but can be restored by switching back to creating a Post</string>
<string name="story_duration">Story Duration</string>
</resources>