2021-07-12 10:14:26 +02:00
|
|
|
package audio.funkwhale.ffa.adapters
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
import android.annotation.SuppressLint
|
|
|
|
import android.content.Context
|
2020-06-22 21:48:31 +02:00
|
|
|
import android.graphics.PorterDuff
|
|
|
|
import android.graphics.PorterDuffColorFilter
|
2020-05-29 13:19:28 +02:00
|
|
|
import android.graphics.Typeface
|
|
|
|
import android.os.Build
|
|
|
|
import android.view.Gravity
|
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewGroup
|
|
|
|
import androidx.appcompat.widget.PopupMenu
|
2023-01-10 13:56:20 +01:00
|
|
|
import androidx.fragment.app.Fragment
|
2020-05-29 13:19:28 +02:00
|
|
|
import androidx.recyclerview.widget.RecyclerView
|
2021-07-12 10:14:26 +02:00
|
|
|
import audio.funkwhale.ffa.R
|
2021-07-16 10:03:52 +02:00
|
|
|
import audio.funkwhale.ffa.databinding.RowSearchHeaderBinding
|
|
|
|
import audio.funkwhale.ffa.databinding.RowTrackBinding
|
2021-08-22 09:48:33 +02:00
|
|
|
import audio.funkwhale.ffa.model.Album
|
|
|
|
import audio.funkwhale.ffa.model.Artist
|
2021-09-09 09:56:15 +02:00
|
|
|
import audio.funkwhale.ffa.model.Track
|
2021-07-16 10:03:52 +02:00
|
|
|
import audio.funkwhale.ffa.utils.Command
|
|
|
|
import audio.funkwhale.ffa.utils.CommandBus
|
2023-01-10 11:00:41 +01:00
|
|
|
import audio.funkwhale.ffa.utils.CoverArt
|
2021-07-16 10:03:52 +02:00
|
|
|
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
|
|
|
|
import audio.funkwhale.ffa.utils.onApi
|
|
|
|
import audio.funkwhale.ffa.utils.toast
|
2023-01-10 13:56:20 +01:00
|
|
|
import audio.funkwhale.ffa.viewmodel.SearchViewModel
|
|
|
|
import com.squareup.picasso.Picasso
|
2020-05-29 13:19:28 +02:00
|
|
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
|
|
|
|
2021-07-16 10:03:52 +02:00
|
|
|
class SearchAdapter(
|
2023-01-10 13:56:20 +01:00
|
|
|
viewModel: SearchViewModel,
|
|
|
|
private val fragment: Fragment,
|
2021-09-10 13:36:28 +02:00
|
|
|
private val listener: OnSearchResultClickListener,
|
|
|
|
private val favoriteListener: FavoriteListener
|
2021-07-16 10:03:52 +02:00
|
|
|
) : RecyclerView.Adapter<SearchAdapter.ViewHolder>() {
|
|
|
|
|
2020-05-30 14:12:04 +02:00
|
|
|
interface OnSearchResultClickListener {
|
|
|
|
fun onArtistClick(holder: View?, artist: Artist)
|
|
|
|
fun onAlbumClick(holder: View?, album: Album)
|
|
|
|
}
|
|
|
|
|
2020-05-29 13:19:28 +02:00
|
|
|
enum class ResultType {
|
|
|
|
Header,
|
|
|
|
Artist,
|
|
|
|
Album,
|
|
|
|
Track
|
|
|
|
}
|
|
|
|
|
2021-07-16 10:03:52 +02:00
|
|
|
private lateinit var searchHeaderBinding: RowSearchHeaderBinding
|
|
|
|
private lateinit var rowTrackBinding: RowTrackBinding
|
|
|
|
|
|
|
|
val sectionCount = 3
|
2020-05-29 13:19:28 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
var artists = listOf<Artist>()
|
|
|
|
var albums = listOf<Album>()
|
|
|
|
var tracks = listOf<Track>()
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
var currentTrack: Track? = null
|
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
init {
|
|
|
|
viewModel.artistResults.observe(fragment.viewLifecycleOwner) {
|
|
|
|
artists = it
|
|
|
|
this.notifyDataSetChanged()
|
|
|
|
}
|
|
|
|
viewModel.albumResults.observe(fragment.viewLifecycleOwner) {
|
|
|
|
albums = it
|
|
|
|
this.notifyDataSetChanged()
|
|
|
|
}
|
|
|
|
viewModel.trackResults.observe(fragment.viewLifecycleOwner) {
|
|
|
|
tracks = it
|
|
|
|
this.notifyDataSetChanged()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-16 10:03:52 +02:00
|
|
|
override fun getItemCount() = sectionCount + artists.size + albums.size + tracks.size
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
override fun getItemId(position: Int): Long {
|
|
|
|
return when (getItemViewType(position)) {
|
|
|
|
ResultType.Header.ordinal -> {
|
|
|
|
if (position == 0) return -1
|
|
|
|
if (position == (artists.size + 1)) return -2
|
|
|
|
return -3
|
|
|
|
}
|
|
|
|
|
|
|
|
ResultType.Artist.ordinal -> artists[position].id.toLong()
|
2023-01-10 13:56:20 +01:00
|
|
|
ResultType.Album.ordinal -> albums[position - artists.size - 2].id.toLong()
|
2021-09-09 09:56:15 +02:00
|
|
|
ResultType.Track.ordinal ->
|
|
|
|
tracks[position - artists.size - albums.size - sectionCount].id.toLong()
|
2020-05-29 13:19:28 +02:00
|
|
|
else -> 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-09 09:56:15 +02:00
|
|
|
override fun getItemViewType(position: Int): Int = when {
|
|
|
|
position == 0 ||
|
|
|
|
position == (artists.size + 1) ||
|
|
|
|
position == (artists.size + albums.size + 2) -> ResultType.Header.ordinal
|
|
|
|
position <= artists.size -> ResultType.Artist.ordinal
|
|
|
|
position <= artists.size + albums.size + 2 -> ResultType.Album.ordinal
|
|
|
|
else -> ResultType.Track.ordinal
|
2020-05-29 13:19:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
2021-07-16 10:03:52 +02:00
|
|
|
return when (viewType) {
|
|
|
|
ResultType.Header.ordinal -> {
|
2023-01-10 13:56:20 +01:00
|
|
|
searchHeaderBinding = RowSearchHeaderBinding.inflate(fragment.layoutInflater, parent, false)
|
|
|
|
SearchHeaderViewHolder(searchHeaderBinding, fragment.requireContext())
|
2021-07-16 10:03:52 +02:00
|
|
|
}
|
|
|
|
else -> {
|
2023-01-10 13:56:20 +01:00
|
|
|
rowTrackBinding = RowTrackBinding.inflate(fragment.layoutInflater, parent, false)
|
|
|
|
RowTrackViewHolder(rowTrackBinding, fragment.requireContext()).also {
|
2021-07-16 10:03:52 +02:00
|
|
|
rowTrackBinding.root.setOnClickListener(it)
|
|
|
|
}
|
|
|
|
}
|
2020-05-29 13:19:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("NewApi")
|
|
|
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
|
|
val resultType = getItemViewType(position)
|
2021-07-16 10:03:52 +02:00
|
|
|
val searchHeaderViewHolder = holder as? SearchHeaderViewHolder
|
|
|
|
val rowTrackViewHolder = holder as? RowTrackViewHolder
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
if (resultType == ResultType.Header.ordinal) {
|
2023-01-10 13:56:20 +01:00
|
|
|
if (position == 0) {
|
|
|
|
searchHeaderViewHolder?.title?.text = fragment.requireContext().getString(R.string.artists)
|
|
|
|
holder.itemView.visibility = View.VISIBLE
|
|
|
|
holder.itemView.layoutParams = RecyclerView.LayoutParams(
|
|
|
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
|
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
|
|
)
|
|
|
|
|
|
|
|
if (artists.isEmpty()) {
|
|
|
|
holder.itemView.visibility = View.GONE
|
|
|
|
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
|
2020-05-30 14:15:59 +02:00
|
|
|
}
|
2023-01-10 13:56:20 +01:00
|
|
|
}
|
2020-05-30 14:15:59 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
if (position == (artists.size + 1)) {
|
|
|
|
searchHeaderViewHolder?.title?.text = fragment.requireContext().getString(R.string.albums)
|
|
|
|
holder.itemView.visibility = View.VISIBLE
|
|
|
|
holder.itemView.layoutParams = RecyclerView.LayoutParams(
|
|
|
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
|
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
|
|
)
|
|
|
|
|
|
|
|
if (albums.isEmpty()) {
|
|
|
|
holder.itemView.visibility = View.GONE
|
|
|
|
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
|
2020-05-30 14:15:59 +02:00
|
|
|
}
|
2023-01-10 13:56:20 +01:00
|
|
|
}
|
2020-05-30 14:15:59 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
if (position == (artists.size + albums.size + 2)) {
|
|
|
|
searchHeaderViewHolder?.title?.text = fragment.requireContext().getString(R.string.tracks)
|
|
|
|
holder.itemView.visibility = View.VISIBLE
|
|
|
|
holder.itemView.layoutParams = RecyclerView.LayoutParams(
|
|
|
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
|
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
|
|
)
|
|
|
|
|
|
|
|
if (tracks.isEmpty()) {
|
|
|
|
holder.itemView.visibility = View.GONE
|
|
|
|
holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0)
|
2020-05-30 14:15:59 +02:00
|
|
|
}
|
2020-05-29 13:19:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
val item = when (resultType) {
|
|
|
|
ResultType.Artist.ordinal -> {
|
2021-07-16 10:03:52 +02:00
|
|
|
rowTrackViewHolder?.actions?.visibility = View.GONE
|
|
|
|
rowTrackViewHolder?.favorite?.visibility = View.GONE
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
artists[position - 1]
|
|
|
|
}
|
|
|
|
|
|
|
|
ResultType.Album.ordinal -> {
|
2021-07-16 10:03:52 +02:00
|
|
|
rowTrackViewHolder?.actions?.visibility = View.GONE
|
|
|
|
rowTrackViewHolder?.favorite?.visibility = View.GONE
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
albums[position - artists.size - 2]
|
|
|
|
}
|
|
|
|
|
2021-08-27 15:13:54 +02:00
|
|
|
ResultType.Track.ordinal -> {
|
|
|
|
tracks[position - artists.size - albums.size - sectionCount]
|
|
|
|
}
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
else -> tracks[position]
|
|
|
|
}
|
|
|
|
|
2023-08-21 18:56:11 +02:00
|
|
|
CoverArt.requestCreator(maybeNormalizeUrl(item.cover()))
|
2020-05-29 13:19:28 +02:00
|
|
|
.fit()
|
|
|
|
.transform(RoundedCornersTransformation(16, 0))
|
2021-07-16 10:03:52 +02:00
|
|
|
.into(rowTrackViewHolder?.cover)
|
2020-05-29 13:19:28 +02:00
|
|
|
|
2021-07-23 09:06:12 +02:00
|
|
|
rowTrackViewHolder?.title?.text = item.title()
|
2021-07-16 10:03:52 +02:00
|
|
|
rowTrackViewHolder?.artist?.text = item.subtitle()
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
Build.VERSION_CODES.P.onApi(
|
|
|
|
{
|
2021-07-16 10:03:52 +02:00
|
|
|
searchHeaderViewHolder?.title?.setTypeface(
|
|
|
|
searchHeaderViewHolder.title.typeface,
|
|
|
|
Typeface.DEFAULT.weight
|
|
|
|
)
|
|
|
|
rowTrackViewHolder?.artist?.setTypeface(
|
|
|
|
rowTrackViewHolder.artist.typeface,
|
|
|
|
Typeface.DEFAULT.weight
|
|
|
|
)
|
2020-05-29 13:19:28 +02:00
|
|
|
},
|
|
|
|
{
|
2021-07-16 10:03:52 +02:00
|
|
|
searchHeaderViewHolder?.title?.typeface =
|
|
|
|
Typeface.create(searchHeaderViewHolder?.title?.typeface, Typeface.NORMAL)
|
|
|
|
rowTrackViewHolder?.artist?.typeface =
|
|
|
|
Typeface.create(rowTrackViewHolder?.artist?.typeface, Typeface.NORMAL)
|
2021-09-09 09:56:15 +02:00
|
|
|
}
|
|
|
|
)
|
2020-05-29 13:19:28 +02:00
|
|
|
|
2021-07-16 10:03:52 +02:00
|
|
|
searchHeaderViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
|
2020-06-22 21:48:31 +02:00
|
|
|
|
2021-08-27 15:13:54 +02:00
|
|
|
when (resultType) {
|
2021-09-09 09:56:15 +02:00
|
|
|
ResultType.Artist.ordinal -> {
|
|
|
|
rowTrackViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
|
|
|
|
0, 0, 0, 0
|
|
|
|
)
|
|
|
|
}
|
|
|
|
ResultType.Album.ordinal -> {
|
|
|
|
rowTrackViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
|
|
|
|
0, 0, 0, 0
|
|
|
|
)
|
|
|
|
}
|
|
|
|
ResultType.Track.ordinal -> {
|
|
|
|
(item as? Track)?.let { track ->
|
2023-01-10 13:56:20 +01:00
|
|
|
if (track == currentTrack || track.current) {
|
|
|
|
searchHeaderViewHolder?.title?.setTypeface(
|
|
|
|
searchHeaderViewHolder.title.typeface,
|
|
|
|
Typeface.BOLD
|
|
|
|
)
|
|
|
|
rowTrackViewHolder?.artist?.setTypeface(
|
|
|
|
rowTrackViewHolder.artist.typeface,
|
|
|
|
Typeface.BOLD
|
|
|
|
)
|
|
|
|
}
|
2020-05-29 13:19:28 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
when (track.favorite) {
|
|
|
|
true -> rowTrackViewHolder?.favorite?.setColorFilter(
|
|
|
|
fragment.requireContext().getColor(R.color.colorFavorite)
|
|
|
|
)
|
|
|
|
false -> rowTrackViewHolder?.favorite?.setColorFilter(
|
|
|
|
fragment.requireContext().getColor(R.color.colorSelected)
|
|
|
|
)
|
|
|
|
}
|
2020-05-29 13:19:28 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
rowTrackViewHolder?.favorite?.setOnClickListener {
|
|
|
|
favoriteListener.let {
|
|
|
|
favoriteListener.onToggleFavorite(track.id, !track.favorite)
|
2020-05-29 13:19:28 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
tracks[position - artists.size - albums.size - sectionCount].favorite =
|
|
|
|
!track.favorite
|
2020-06-22 21:48:31 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
notifyItemChanged(position)
|
2021-09-09 09:56:15 +02:00
|
|
|
}
|
2023-01-10 13:56:20 +01:00
|
|
|
}
|
2020-06-22 21:48:31 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
when (track.cached || track.downloaded) {
|
|
|
|
true -> rowTrackViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
|
|
|
|
R.drawable.downloaded, 0, 0, 0
|
|
|
|
)
|
|
|
|
false -> rowTrackViewHolder?.title?.setCompoundDrawablesWithIntrinsicBounds(
|
|
|
|
0, 0, 0, 0
|
|
|
|
)
|
|
|
|
}
|
2020-06-22 21:48:31 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
if (track.cached && !track.downloaded) {
|
|
|
|
rowTrackViewHolder?.title?.compoundDrawables?.forEach {
|
|
|
|
it?.colorFilter =
|
|
|
|
PorterDuffColorFilter(
|
|
|
|
fragment.requireContext().getColor(R.color.cached),
|
|
|
|
PorterDuff.Mode.SRC_IN
|
|
|
|
)
|
2021-09-09 09:56:15 +02:00
|
|
|
}
|
2023-01-10 13:56:20 +01:00
|
|
|
}
|
2020-05-29 13:19:28 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
if (track.downloaded) {
|
|
|
|
rowTrackViewHolder?.title?.compoundDrawables?.forEach {
|
|
|
|
it?.colorFilter =
|
|
|
|
PorterDuffColorFilter(
|
|
|
|
fragment.requireContext().getColor(R.color.downloaded),
|
|
|
|
PorterDuff.Mode.SRC_IN
|
|
|
|
)
|
2021-09-09 09:56:15 +02:00
|
|
|
}
|
2023-01-10 13:56:20 +01:00
|
|
|
}
|
2020-05-29 13:19:28 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
rowTrackViewHolder?.actions?.setOnClickListener {
|
|
|
|
PopupMenu(
|
|
|
|
fragment.requireContext(),
|
|
|
|
rowTrackViewHolder.actions,
|
|
|
|
Gravity.START,
|
|
|
|
R.attr.actionOverflowMenuStyle,
|
|
|
|
0
|
|
|
|
).apply {
|
|
|
|
inflate(R.menu.row_track)
|
|
|
|
|
|
|
|
setOnMenuItemClickListener {
|
|
|
|
when (it.itemId) {
|
|
|
|
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
|
|
|
|
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
|
|
|
|
R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
|
|
|
|
R.id.track_add_to_playlist -> CommandBus.send(
|
|
|
|
Command.AddToPlaylist(listOf(track))
|
|
|
|
)
|
|
|
|
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
|
2021-08-27 15:13:54 +02:00
|
|
|
}
|
2021-09-09 09:56:15 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
true
|
2021-08-27 15:13:54 +02:00
|
|
|
}
|
2023-01-10 13:56:20 +01:00
|
|
|
|
|
|
|
show()
|
2020-05-29 13:19:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-09-09 09:56:15 +02:00
|
|
|
}
|
2020-05-29 13:19:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-11 17:41:41 +02:00
|
|
|
fun getPositionOf(type: ResultType, position: Int): Int {
|
|
|
|
return when (type) {
|
|
|
|
ResultType.Artist -> position + 1
|
|
|
|
ResultType.Album -> position + artists.size + 2
|
2021-07-16 10:03:52 +02:00
|
|
|
ResultType.Track -> artists.size + albums.size + sectionCount + position
|
2020-07-11 17:41:41 +02:00
|
|
|
else -> 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
inner class SearchHeaderViewHolder(val binding: RowSearchHeaderBinding, context: Context) :
|
2021-07-16 10:03:52 +02:00
|
|
|
ViewHolder(binding.root, context) {
|
|
|
|
val title = binding.title
|
|
|
|
}
|
2020-05-29 13:19:28 +02:00
|
|
|
|
2023-01-10 13:56:20 +01:00
|
|
|
inner class RowTrackViewHolder(val binding: RowTrackBinding, context: Context) :
|
2021-07-16 10:03:52 +02:00
|
|
|
ViewHolder(binding.root, context), View.OnClickListener {
|
2021-07-23 09:06:12 +02:00
|
|
|
val title = binding.title
|
2021-07-16 10:03:52 +02:00
|
|
|
val cover = binding.cover
|
|
|
|
val artist = binding.artist
|
|
|
|
|
|
|
|
val favorite = binding.favorite
|
|
|
|
val actions = binding.actions
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
override fun onClick(view: View?) {
|
|
|
|
when (getItemViewType(layoutPosition)) {
|
2020-05-30 14:12:04 +02:00
|
|
|
ResultType.Artist.ordinal -> {
|
|
|
|
val position = layoutPosition - 1
|
|
|
|
|
2021-09-10 13:36:28 +02:00
|
|
|
listener.onArtistClick(view, artists[position])
|
2020-05-30 14:12:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ResultType.Album.ordinal -> {
|
|
|
|
val position = layoutPosition - artists.size - 2
|
|
|
|
|
2021-09-10 13:36:28 +02:00
|
|
|
listener.onAlbumClick(view, albums[position])
|
2020-05-30 14:12:04 +02:00
|
|
|
}
|
|
|
|
|
2020-05-29 13:19:28 +02:00
|
|
|
ResultType.Track.ordinal -> {
|
2021-07-16 10:03:52 +02:00
|
|
|
val position = layoutPosition - artists.size - albums.size - sectionCount
|
2020-05-29 13:19:28 +02:00
|
|
|
|
|
|
|
tracks.subList(position, tracks.size).plus(tracks.subList(0, position)).apply {
|
|
|
|
CommandBus.send(Command.ReplaceQueue(this))
|
|
|
|
|
|
|
|
context.toast("All tracks were added to your queue")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
else -> {
|
2021-07-16 10:03:52 +02:00
|
|
|
// empty
|
2020-05-29 13:19:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-07-16 10:03:52 +02:00
|
|
|
|
|
|
|
abstract inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view)
|
2020-05-29 13:19:28 +02:00
|
|
|
}
|