Skip to content

Commit

Permalink
Twelve: Activity UI
Browse files Browse the repository at this point in the history
Change-Id: I983c648669a1fe1a134c8cbe44518352b8767deb
  • Loading branch information
SebaUbuntu committed Nov 21, 2024
1 parent 86e6c4d commit 3080f0d
Show file tree
Hide file tree
Showing 9 changed files with 574 additions and 2 deletions.
113 changes: 113 additions & 0 deletions app/src/main/java/org/lineageos/twelve/fragments/ActivityFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,92 @@
package org.lineageos.twelve.fragments

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.LinearLayout
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.lineageos.twelve.R
import org.lineageos.twelve.ext.getViewProperty
import org.lineageos.twelve.ext.setProgressCompat
import org.lineageos.twelve.models.ActivityTab
import org.lineageos.twelve.models.Album
import org.lineageos.twelve.models.Artist
import org.lineageos.twelve.models.Audio
import org.lineageos.twelve.models.Genre
import org.lineageos.twelve.models.Playlist
import org.lineageos.twelve.models.RequestStatus
import org.lineageos.twelve.ui.recyclerview.SimpleListAdapter
import org.lineageos.twelve.ui.recyclerview.UniqueItemDiffCallback
import org.lineageos.twelve.ui.views.ActivityTabView
import org.lineageos.twelve.utils.PermissionsChecker
import org.lineageos.twelve.utils.PermissionsUtils
import org.lineageos.twelve.viewmodels.ActivityViewModel

/**
* User activity, notifications and recommendations.
*/
class ActivityFragment : Fragment(R.layout.fragment_activity) {
// View models
private val viewModel by viewModels<ActivityViewModel>()

// Views
private val linearProgressIndicator by getViewProperty<LinearProgressIndicator>(R.id.linearProgressIndicator)
private val noElementsLinearLayout by getViewProperty<LinearLayout>(R.id.noElementsLinearLayout)
private val recyclerView by getViewProperty<RecyclerView>(R.id.recyclerView)

// RecyclerView
private val adapter by lazy {
object : SimpleListAdapter<ActivityTab, ActivityTabView>(
UniqueItemDiffCallback(),
::ActivityTabView,
) {
override fun ViewHolder.onPrepareView() {
view.setOnItemClickListener { items, position ->
when (val item = items[position]) {
is Album -> findNavController().navigate(
R.id.action_mainFragment_to_fragment_album,
AlbumFragment.createBundle(item.uri)
)

is Artist -> findNavController().navigate(
R.id.action_mainFragment_to_fragment_artist,
ArtistFragment.createBundle(item.uri)
)

is Audio -> findNavController().navigate(
R.id.action_mainFragment_to_fragment_audio_bottom_sheet_dialog,
AudioBottomSheetDialogFragment.createBundle(item.uri)
)

is Genre -> findNavController().navigate(
R.id.action_mainFragment_to_fragment_genre,
GenreFragment.createBundle(item.uri)
)

is Playlist -> findNavController().navigate(
R.id.action_mainFragment_to_fragment_playlist,
PlaylistFragment.createBundle(item.uri)
)
}
}
}

override fun ViewHolder.onBindView(item: ActivityTab) {
view.setActivityTab(item)
}
}
}

// Permissions
private val permissionsChecker = PermissionsChecker(
this, PermissionsUtils.mainPermissions
Expand All @@ -28,12 +100,53 @@ class ActivityFragment : Fragment(R.layout.fragment_activity) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

recyclerView.adapter = adapter

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
permissionsChecker.withPermissionsGranted {
loadData()
}
}
}
}

override fun onDestroyView() {
recyclerView.adapter = null

super.onDestroyView()
}

private suspend fun loadData() {
viewModel.activity.collectLatest {
linearProgressIndicator.setProgressCompat(it, true)

when (it) {
is RequestStatus.Loading -> {
// Do nothing
}

is RequestStatus.Success -> {
val data = it.data

adapter.submitList(data)

val isEmpty = it.data.isEmpty()
recyclerView.isVisible = !isEmpty
noElementsLinearLayout.isVisible = isEmpty
}

is RequestStatus.Error -> {
Log.e(LOG_TAG, "Failed to load activity, error: ${it.error}")

recyclerView.isVisible = false
noElementsLinearLayout.isVisible = true
}
}
}
}

companion object {
private val LOG_TAG = ActivityFragment::class.simpleName!!
}
}
171 changes: 171 additions & 0 deletions app/src/main/java/org/lineageos/twelve/ui/views/ActivityTabItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* SPDX-FileCopyrightText: 2024 The LineageOS Project
* SPDX-License-Identifier: Apache-2.0
*/

package org.lineageos.twelve.ui.views

import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.AttrRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import coil3.load
import coil3.request.ImageRequest
import com.google.android.material.card.MaterialCardView
import org.lineageos.twelve.R
import org.lineageos.twelve.models.Album
import org.lineageos.twelve.models.Artist
import org.lineageos.twelve.models.Audio
import org.lineageos.twelve.models.Genre
import org.lineageos.twelve.models.MediaItem
import org.lineageos.twelve.models.Playlist

class ActivityTabItem @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = com.google.android.material.R.attr.materialCardViewStyle,
) : MaterialCardView(context, attrs, defStyleAttr) {
private val headlineTextView by lazy { findViewById<TextView>(R.id.headlineTextView) }
private val placeholderImageView by lazy { findViewById<ImageView>(R.id.placeholderImageView) }
private val subheadTextView by lazy { findViewById<TextView>(R.id.subheadTextView) }
private val supportingTextView by lazy { findViewById<TextView>(R.id.supportingTextView) }
private val thumbnailImageView by lazy { findViewById<ImageView>(R.id.thumbnailImageView) }

private var headlineText: CharSequence?
get() = headlineTextView.text
set(value) {
headlineTextView.setTextAndUpdateVisibility(value)
}

private var subheadText: CharSequence?
get() = subheadTextView.text
set(value) {
subheadTextView.setTextAndUpdateVisibility(value)
}

private var supportingText: CharSequence?
get() = supportingTextView.text
set(value) {
supportingTextView.setTextAndUpdateVisibility(value)
}

init {
setCardBackgroundColor(Color.TRANSPARENT)
cardElevation = 0f
strokeWidth = 0

inflate(context, R.layout.item_activity_tab, this)
}

fun setItem(item: MediaItem<*>) {
when (item) {
is Album -> {
headlineText = item.title
subheadText = item.artistName
supportingText = item.year?.toString()

item.thumbnail?.uri?.let {
loadThumbnailImage(it, R.drawable.ic_album)
} ?: item.thumbnail?.bitmap?.let {
loadThumbnailImage(it, R.drawable.ic_album)
} ?: setPlaceholderImage(R.drawable.ic_album)
}

is Artist -> {
headlineText = item.name
subheadText = null
supportingText = null

item.thumbnail?.uri?.let {
loadThumbnailImage(it)
} ?: item.thumbnail?.bitmap?.let {
loadThumbnailImage(it)
} ?: setPlaceholderImage(R.drawable.ic_person)
}

is Audio -> {
headlineText = item.title
subheadText = item.artistName
supportingText = item.albumTitle

setPlaceholderImage(R.drawable.ic_music_note)
}

is Genre -> {
item.name?.let {
headlineText = it
} ?: setHeadlineText(R.string.genre_unknown)
subheadText = null
supportingText = null

setPlaceholderImage(R.drawable.ic_genres)
}

is Playlist -> {
headlineText = item.name
subheadText = null
supportingText = null

setPlaceholderImage(R.drawable.ic_playlist_play)
}
}
}

private fun setPlaceholderImage(@DrawableRes placeholder: Int) {
placeholderImageView.setImageResource(placeholder)
placeholderImageView.isVisible = true
thumbnailImageView.isVisible = false
}

private fun loadThumbnailImage(
data: Any?,
@DrawableRes placeholder: Int? = null,
builder: ImageRequest.Builder.() -> Unit = {
listener(
onCancel = {
placeholder?.let {
placeholderImageView.setImageResource(it)
placeholderImageView.isVisible = true
}
thumbnailImageView.isVisible = false
},
onError = { _, _ ->
placeholder?.let {
placeholderImageView.setImageResource(it)
placeholderImageView.isVisible = true
}
thumbnailImageView.isVisible = false
},
onSuccess = { _, _ ->
placeholderImageView.isVisible = false
thumbnailImageView.isVisible = true
},
)
}
) = thumbnailImageView.load(data, builder = builder)

private fun setHeadlineText(@StringRes resId: Int) =
headlineTextView.setTextAndUpdateVisibility(resId)

private fun setSubheadText(@StringRes resId: Int) =
subheadTextView.setTextAndUpdateVisibility(resId)

private fun setSupportingText(@StringRes resId: Int) =
supportingTextView.setTextAndUpdateVisibility(resId)

// TextView utils

private fun TextView.setTextAndUpdateVisibility(text: CharSequence?) {
this.text = text.also {
isVisible = it != null
}
}

private fun TextView.setTextAndUpdateVisibility(@StringRes resId: Int) =
setTextAndUpdateVisibility(resources.getText(resId))
}
Loading

0 comments on commit 3080f0d

Please sign in to comment.