list: add type-specific menu impls
Add menus for each fundamental music datatype. Each will display information about the data contained.
This commit is contained in:
parent
c1158b1a07
commit
db2e9e12f0
9 changed files with 361 additions and 27 deletions
|
@ -22,34 +22,71 @@ import android.annotation.SuppressLint
|
|||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.appcompat.view.menu.MenuBuilder
|
||||
import androidx.core.view.children
|
||||
import org.oxycblt.auxio.R
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.DialogMenuBinding
|
||||
import org.oxycblt.auxio.list.ClickableListListener
|
||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.ui.ViewBindingBottomSheetDialogFragment
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* A [ViewBindingBottomSheetDialogFragment] that displays basic music information and
|
||||
* a series of options.
|
||||
* A [ViewBindingBottomSheetDialogFragment] that displays basic music information and a series of
|
||||
* options.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class MenuDialogFragment : ViewBindingBottomSheetDialogFragment<DialogMenuBinding>() {
|
||||
private val menuAdapter = MenuOptionAdapter()
|
||||
abstract class MenuDialogFragment<T : Music> :
|
||||
ViewBindingBottomSheetDialogFragment<DialogMenuBinding>(), ClickableListListener<MenuItem> {
|
||||
protected abstract val menuModel: MenuViewModel
|
||||
private val menuAdapter = MenuOptionAdapter(@Suppress("LeakingThis") this)
|
||||
|
||||
abstract val menuRes: Int
|
||||
abstract val uid: Music.UID
|
||||
abstract fun updateMusic(binding: DialogMenuBinding, music: T)
|
||||
abstract fun onClick(music: T, @IdRes optionId: Int)
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) = DialogMenuBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: DialogMenuBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
binding.menuRecycler.apply {
|
||||
// --- UI SETUP ---
|
||||
binding.menuOptionRecycler.apply {
|
||||
adapter = menuAdapter
|
||||
itemAnimator = null
|
||||
}
|
||||
|
||||
// Avoid having to use a dummy view and rely on what AndroidX Toolbar uses.
|
||||
@SuppressLint("RestrictedApi") val builder = MenuBuilder(requireContext())
|
||||
MenuInflater(requireContext()).inflate(R.menu.item_song, builder)
|
||||
MenuInflater(requireContext()).inflate(menuRes, builder)
|
||||
menuAdapter.update(builder.children.toList(), UpdateInstructions.Diff)
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
menuModel.setMusic(uid)
|
||||
collectImmediately(menuModel.currentMusic, this::updateMusic)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: DialogMenuBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.menuOptionRecycler.adapter = null
|
||||
}
|
||||
|
||||
private fun updateMusic(music: Music?) {
|
||||
if (music == null) {
|
||||
logD("No music to show, navigating away")
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST") updateMusic(requireBinding(), music as T)
|
||||
}
|
||||
|
||||
final override fun onClick(item: MenuItem, viewHolder: RecyclerView.ViewHolder) {
|
||||
@Suppress("UNCHECKED_CAST") onClick(menuModel.currentMusic.value as T, item.itemId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* MenuDialogFragmentImpl.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.list.menu
|
||||
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogMenuBinding
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.resolveNames
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SongMenuDialogFragment : MenuDialogFragment<Song>() {
|
||||
override val menuModel: MenuViewModel by viewModels()
|
||||
private val args: SongMenuDialogFragmentArgs by navArgs()
|
||||
|
||||
override val menuRes = args.menuRes
|
||||
override val uid = args.songUid
|
||||
|
||||
override fun updateMusic(binding: DialogMenuBinding, music: Song) {
|
||||
val context = requireContext()
|
||||
binding.menuCover.bind(music)
|
||||
binding.menuInfo.text = getString(R.string.lbl_song)
|
||||
binding.menuName.text = music.name.resolve(context)
|
||||
binding.menuInfo.text = music.artists.resolveNames(context)
|
||||
}
|
||||
|
||||
override fun onClick(music: Song, optionId: Int) {}
|
||||
}
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AlbumMenuDialogFragment : MenuDialogFragment<Album>() {
|
||||
override val menuModel: MenuViewModel by viewModels()
|
||||
private val args: AlbumMenuDialogFragmentArgs by navArgs()
|
||||
|
||||
override val menuRes = args.menuRes
|
||||
override val uid = args.albumUid
|
||||
|
||||
override fun updateMusic(binding: DialogMenuBinding, music: Album) {
|
||||
val context = requireContext()
|
||||
binding.menuCover.bind(music)
|
||||
binding.menuInfo.text = getString(music.releaseType.stringRes)
|
||||
binding.menuName.text = music.name.resolve(context)
|
||||
binding.menuInfo.text = music.artists.resolveNames(context)
|
||||
}
|
||||
|
||||
override fun onClick(music: Album, optionId: Int) {}
|
||||
}
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ArtistMenuDialogFragment : MenuDialogFragment<Artist>() {
|
||||
override val menuModel: MenuViewModel by viewModels()
|
||||
private val args: ArtistMenuDialogFragmentArgs by navArgs()
|
||||
|
||||
override val menuRes = args.menuRes
|
||||
override val uid = args.artistUid
|
||||
|
||||
override fun updateMusic(binding: DialogMenuBinding, music: Artist) {
|
||||
val context = requireContext()
|
||||
binding.menuCover.bind(music)
|
||||
binding.menuInfo.text = getString(R.string.lbl_artist)
|
||||
binding.menuName.text = music.name.resolve(context)
|
||||
binding.menuInfo.text =
|
||||
getString(
|
||||
R.string.fmt_two,
|
||||
context.getPlural(R.plurals.fmt_album_count, music.albums.size),
|
||||
if (music.songs.isNotEmpty()) {
|
||||
context.getPlural(R.plurals.fmt_song_count, music.songs.size)
|
||||
} else {
|
||||
getString(R.string.def_song_count)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onClick(music: Artist, optionId: Int) {}
|
||||
}
|
||||
|
||||
@AndroidEntryPoint
|
||||
class GenreMenuDialogFragment : MenuDialogFragment<Genre>() {
|
||||
override val menuModel: MenuViewModel by viewModels()
|
||||
private val args: GenreMenuDialogFragmentArgs by navArgs()
|
||||
|
||||
override val menuRes = args.menuRes
|
||||
override val uid = args.genreUid
|
||||
|
||||
override fun updateMusic(binding: DialogMenuBinding, music: Genre) {
|
||||
val context = requireContext()
|
||||
binding.menuCover.bind(music)
|
||||
binding.menuInfo.text = getString(R.string.lbl_genre)
|
||||
binding.menuName.text = music.name.resolve(context)
|
||||
binding.menuInfo.text =
|
||||
getString(
|
||||
R.string.fmt_two,
|
||||
context.getPlural(R.plurals.fmt_artist_count, music.artists.size),
|
||||
context.getPlural(R.plurals.fmt_song_count, music.songs.size))
|
||||
}
|
||||
|
||||
override fun onClick(music: Genre, optionId: Int) {}
|
||||
}
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PlaylistMenuDialogFragment : MenuDialogFragment<Playlist>() {
|
||||
override val menuModel: MenuViewModel by viewModels()
|
||||
private val args: PlaylistMenuDialogFragmentArgs by navArgs()
|
||||
|
||||
override val menuRes = args.menuRes
|
||||
override val uid = args.playlistUid
|
||||
|
||||
override fun updateMusic(binding: DialogMenuBinding, music: Playlist) {
|
||||
val context = requireContext()
|
||||
binding.menuCover.bind(music)
|
||||
binding.menuInfo.text = getString(R.string.lbl_genre)
|
||||
binding.menuName.text = music.name.resolve(context)
|
||||
binding.menuInfo.text = context.getPlural(R.plurals.fmt_song_count, music.songs.size)
|
||||
}
|
||||
|
||||
override fun onClick(music: Playlist, optionId: Int) {}
|
||||
}
|
|
@ -22,15 +22,18 @@ import android.view.MenuItem
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.oxycblt.auxio.databinding.ItemMenuOptionBinding
|
||||
import org.oxycblt.auxio.list.ClickableListListener
|
||||
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
|
||||
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/**
|
||||
* Displays a list of [MenuItem]s as custom list items.
|
||||
*
|
||||
* @param listener A [MenuOptionAdapter] to bind interactions to.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class MenuOptionAdapter :
|
||||
class MenuOptionAdapter(private val listener: ClickableListListener<MenuItem>) :
|
||||
FlexibleListAdapter<MenuItem, MenuOptionViewHolder>(MenuOptionViewHolder.DIFF_CALLBACK) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
MenuOptionViewHolder.from(parent)
|
||||
|
@ -42,6 +45,7 @@ class MenuOptionAdapter :
|
|||
|
||||
/**
|
||||
* A [DialogRecyclerView.ViewHolder] that displays a list of menu options based on [MenuItem].
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class MenuOptionViewHolder private constructor(private val binding: ItemMenuOptionBinding) :
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* MenuViewModel.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.list.menu
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
@HiltViewModel
|
||||
class MenuViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
||||
ViewModel(), MusicRepository.UpdateListener {
|
||||
private val _currentMusic = MutableStateFlow<Music?>(null)
|
||||
val currentMusic: StateFlow<Music?> = _currentMusic
|
||||
|
||||
init {
|
||||
musicRepository.addUpdateListener(this)
|
||||
}
|
||||
|
||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||
_currentMusic.value = _currentMusic.value?.let { musicRepository.find(it.uid) }
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
musicRepository.removeUpdateListener(this)
|
||||
}
|
||||
|
||||
fun setMusic(uid: Music.UID) {
|
||||
_currentMusic.value = musicRepository.find(uid)
|
||||
if (_currentMusic.value == null) {
|
||||
logW("Given Music UID to show was invalid")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||
style="@style/Widget.Auxio.RecyclerView.Linear"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.oxycblt.auxio.image.CoverView
|
||||
android:id="@+id/menu_cover"
|
||||
style="@style/Widget.Auxio.Image.MidLarge"
|
||||
android:layout_margin="@dimen/spacing_medium"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/menu_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/spacing_medium"
|
||||
android:paddingHorizontal="@dimen/spacing_medium"
|
||||
android:textAppearance="@style/TextAppearance.Auxio.HeadlineSmall"
|
||||
android:text="TODO: Put info here" />
|
||||
android:textAppearance="@style/TextAppearance.Auxio.LabelLarge"
|
||||
android:textColor="?attr/colorSecondary"
|
||||
android:layout_marginStart="@dimen/spacing_medium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/menu_name"
|
||||
app:layout_constraintStart_toEndOf="@+id/menu_cover"
|
||||
app:layout_constraintTop_toTopOf="@+id/menu_cover"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Type" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/menu_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Auxio.TitleLarge"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/menu_info"
|
||||
app:layout_constraintStart_toStartOf="@+id/menu_type"
|
||||
app:layout_constraintTop_toBottomOf="@+id/menu_type"
|
||||
tools:text="Name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/menu_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Auxio.TitleMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/menu_cover"
|
||||
app:layout_constraintStart_toStartOf="@+id/menu_name"
|
||||
app:layout_constraintTop_toBottomOf="@+id/menu_name"
|
||||
tools:text="Info A" />
|
||||
|
||||
<org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
||||
android:id="@+id/menu_recycler"
|
||||
android:id="@+id/menu_option_recycler"
|
||||
style="@style/Widget.Auxio.RecyclerView.Linear"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/menu_cover"
|
||||
tools:listitem="@layout/item_menu_option" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -30,9 +30,6 @@
|
|||
<action
|
||||
android:id="@+id/show_genre"
|
||||
app:destination="@id/genre_detail_fragment" />
|
||||
<action
|
||||
android:id="@+id/open_menu"
|
||||
app:destination="@id/menu_dialog" />
|
||||
<action
|
||||
android:id="@+id/show_playlist"
|
||||
app:destination="@id/playlist_detail_fragment" />
|
||||
|
@ -301,10 +298,69 @@
|
|||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/menu_dialog"
|
||||
android:name="org.oxycblt.auxio.list.menu.MenuDialogFragment"
|
||||
android:label="menu_dialog"
|
||||
tools:layout="@layout/dialog_menu" />
|
||||
android:id="@+id/song_menu_dialog"
|
||||
android:name="org.oxycblt.auxio.list.menu.SongMenuDialogFragment"
|
||||
android:label="song_menu_dialog"
|
||||
tools:layout="@layout/dialog_menu">
|
||||
<argument
|
||||
android:name="menuRes"
|
||||
app:argType="integer" />
|
||||
<argument
|
||||
android:name="songUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/album_menu_dialog"
|
||||
android:name="org.oxycblt.auxio.list.menu.AlbumMenuDialogFragment"
|
||||
android:label="album_menu_dialog"
|
||||
tools:layout="@layout/dialog_menu">
|
||||
<argument
|
||||
android:name="menuRes"
|
||||
app:argType="integer"/>
|
||||
<argument
|
||||
android:name="albumUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/artist_menu_dialog"
|
||||
android:name="org.oxycblt.auxio.list.menu.ArtistMenuDialogFragment"
|
||||
android:label="artist_menu_dialog"
|
||||
tools:layout="@layout/dialog_menu">
|
||||
<argument
|
||||
android:name="menuRes"
|
||||
app:argType="integer" />
|
||||
<argument
|
||||
android:name="artistUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/genre_menu_dialog"
|
||||
android:name="org.oxycblt.auxio.list.menu.GenreMenuDialogFragment"
|
||||
android:label="genre_menu_dialog"
|
||||
tools:layout="@layout/dialog_menu">
|
||||
<argument
|
||||
android:name="menuRes"
|
||||
app:argType="integer" />
|
||||
<argument
|
||||
android:name="genreUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/playlist_menu_dialog"
|
||||
android:name="org.oxycblt.auxio.list.menu.PlaylistMenuDialogFragment"
|
||||
android:label="playlist_menu_dialog"
|
||||
tools:layout="@layout/dialog_menu">
|
||||
<argument
|
||||
android:name="menuRes"
|
||||
app:argType="integer" />
|
||||
<argument
|
||||
android:name="playlistUid"
|
||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||
</dialog>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/root_preferences_fragment"
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
<!-- Size Namespace | Width & Heights for UI elements -->
|
||||
<dimen name="size_cover_compact">48dp</dimen>
|
||||
<dimen name="size_cover_normal">56dp</dimen>
|
||||
<dimen name="size_cover_medium">56dp</dimen>
|
||||
<dimen name="size_cover_mid_large">92dp</dimen>
|
||||
<dimen name="size_cover_large">128dp</dimen>
|
||||
<dimen name="size_cover_mid_huge">192dp</dimen>
|
||||
<dimen name="size_cover_huge">256dp</dimen>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<string name="lbl_grant">Grant</string>
|
||||
|
||||
<string name="lbl_songs">Songs</string>
|
||||
<string name="lbl_song">Song</string>
|
||||
<string name="lbl_all_songs">All songs</string>
|
||||
|
||||
<string name="lbl_albums">Albums</string>
|
||||
|
|
|
@ -60,8 +60,14 @@
|
|||
</style>
|
||||
|
||||
<style name="Widget.Auxio.Image.Medium" parent="">
|
||||
<item name="android:layout_width">@dimen/size_cover_normal</item>
|
||||
<item name="android:layout_height">@dimen/size_cover_normal</item>
|
||||
<item name="android:layout_width">@dimen/size_cover_medium</item>
|
||||
<item name="android:layout_height">@dimen/size_cover_medium</item>
|
||||
<item name="sizing">medium</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Auxio.Image.MidLarge" parent="">
|
||||
<item name="android:layout_width">@dimen/size_cover_mid_large</item>
|
||||
<item name="android:layout_height">@dimen/size_cover_mid_large</item>
|
||||
<item name="sizing">medium</item>
|
||||
</style>
|
||||
|
||||
|
|
Loading…
Reference in a new issue