mirror of
https://github.com/owntone/owntone-server.git
synced 2025-11-11 14:30:20 -05:00
#1473 Add sort by rating for composer, genre, and artist tracks.
This commit is contained in:
@@ -49,6 +49,7 @@ import ListTracks from '@/components/ListTracks.vue'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
|
||||
import CoverArtwork from '@/components/CoverArtwork.vue'
|
||||
import webapi from '@/webapi'
|
||||
import { GroupByList } from '@/lib/GroupByList'
|
||||
|
||||
const dataObject = {
|
||||
load: function (to) {
|
||||
@@ -60,7 +61,7 @@ const dataObject = {
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.album = response[0].data
|
||||
vm.tracks = response[1].data.items
|
||||
vm.tracks = new GroupByList(response[1].data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +85,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
album: {},
|
||||
tracks: [],
|
||||
|
||||
tracks: new GroupByList(),
|
||||
show_album_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,20 @@
|
||||
<div>
|
||||
<content-with-heading>
|
||||
<template #options>
|
||||
<index-button-list :index="index_list" />
|
||||
<index-button-list :index="tracks.indexList" />
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p
|
||||
class="heading"
|
||||
style="margin-bottom: 24px"
|
||||
v-text="$t('page.artist.sort-by.title')"
|
||||
/>
|
||||
<dropdown-menu
|
||||
v-model="selected_groupby_option_id"
|
||||
:options="groupby_options"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #heading-left>
|
||||
<p class="title is-4" v-text="artist.name" />
|
||||
@@ -39,7 +52,7 @@
|
||||
"
|
||||
/>
|
||||
</p>
|
||||
<list-tracks :tracks="tracks.items" :uris="track_uris" />
|
||||
<list-tracks :tracks="tracks" :uris="track_uris" />
|
||||
<modal-dialog-artist
|
||||
:show="show_artist_details_modal"
|
||||
:artist="artist"
|
||||
@@ -52,10 +65,13 @@
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||
import IndexButtonList from '@/components/IndexButtonList.vue'
|
||||
import ListTracks from '@/components/ListTracks.vue'
|
||||
import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import { byName, byRating, GroupByList } from '@/lib/GroupByList'
|
||||
|
||||
const dataObject = {
|
||||
load: function (to) {
|
||||
@@ -67,7 +83,7 @@ const dataObject = {
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.artist = response[0].data
|
||||
vm.tracks = response[1].data.tracks
|
||||
vm.tracks_list = new GroupByList(response[1].data.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +91,9 @@ export default {
|
||||
name: 'PageArtistTracks',
|
||||
components: {
|
||||
ContentWithHeading,
|
||||
ListTracks,
|
||||
DropdownMenu,
|
||||
IndexButtonList,
|
||||
ListTracks,
|
||||
ModalDialogArtist
|
||||
},
|
||||
|
||||
@@ -96,25 +113,43 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
artist: {},
|
||||
tracks: { items: [] },
|
||||
|
||||
show_artist_details_modal: false
|
||||
groupby_options: [
|
||||
{
|
||||
id: 1,
|
||||
name: this.$t('page.artist.sort-by.name'),
|
||||
options: byName('title_sort')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: this.$t('page.artist.sort-by.rating'),
|
||||
options: byRating('rating', {
|
||||
direction: 'desc'
|
||||
})
|
||||
}
|
||||
],
|
||||
show_artist_details_modal: false,
|
||||
tracks_list: new GroupByList()
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
index_list() {
|
||||
return [
|
||||
...new Set(
|
||||
this.tracks.items.map((track) =>
|
||||
track.title_sort.charAt(0).toUpperCase()
|
||||
)
|
||||
)
|
||||
]
|
||||
selected_groupby_option_id: {
|
||||
get() {
|
||||
return this.$store.state.artist_tracks_sort
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit(types.ARTIST_TRACKS_SORT, value)
|
||||
}
|
||||
},
|
||||
tracks() {
|
||||
const groupBy = this.groupby_options.find(
|
||||
(o) => o.id === this.selected_groupby_option_id
|
||||
)
|
||||
this.tracks_list.group(groupBy.options)
|
||||
return this.tracks_list
|
||||
},
|
||||
|
||||
track_uris() {
|
||||
return this.tracks.items.map((a) => a.uri).join(',')
|
||||
return this.tracks_list.items.map((a) => a.uri).join(',')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -125,10 +160,7 @@ export default {
|
||||
},
|
||||
|
||||
play: function () {
|
||||
webapi.player_play_uri(
|
||||
this.tracks.items.map((a) => a.uri).join(','),
|
||||
true
|
||||
)
|
||||
webapi.player_play_uri(this.tracks_list.map((a) => a.uri).join(','), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ import ListTracks from '@/components/ListTracks.vue'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
|
||||
import CoverArtwork from '@/components/CoverArtwork.vue'
|
||||
import webapi from '@/webapi'
|
||||
import { GroupByList } from '@/lib/GroupByList'
|
||||
|
||||
const dataObject = {
|
||||
load: function (to) {
|
||||
@@ -63,7 +64,7 @@ const dataObject = {
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.album = response[0].data
|
||||
vm.tracks = response[1].data.items
|
||||
vm.tracks = new GroupByList(response[1].data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +88,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
album: {},
|
||||
tracks: [],
|
||||
|
||||
tracks: new GroupByList(),
|
||||
show_album_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<p class="heading" v-text="$t('page.browse.tracks')" />
|
||||
</template>
|
||||
<template #content>
|
||||
<list-tracks :tracks="recently_played.items" />
|
||||
<list-tracks :tracks="recently_played" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<nav class="level">
|
||||
@@ -77,7 +77,7 @@ const dataObject = {
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.recently_added = new GroupByList(response[0].data.albums)
|
||||
vm.recently_played = response[1].data.tracks
|
||||
vm.recently_played = new GroupByList(response[1].data.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<p class="heading" v-text="$t('page.browse.recently-played.tracks')" />
|
||||
</template>
|
||||
<template #content>
|
||||
<list-tracks :tracks="recently_played.items" />
|
||||
<list-tracks :tracks="recently_played" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@@ -21,6 +21,7 @@ import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import TabsMusic from '@/components/TabsMusic.vue'
|
||||
import ListTracks from '@/components/ListTracks.vue'
|
||||
import webapi from '@/webapi'
|
||||
import { GroupByList } from '@/lib/GroupByList'
|
||||
|
||||
const dataObject = {
|
||||
load: function (to) {
|
||||
@@ -33,7 +34,7 @@ const dataObject = {
|
||||
},
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.recently_played = response.data.tracks
|
||||
vm.recently_played = new GroupByList(response.data.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<content-with-heading>
|
||||
<template #options>
|
||||
<index-button-list :index="tracks.indexList" />
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p
|
||||
class="heading"
|
||||
style="margin-bottom: 24px"
|
||||
v-text="$t('page.artist.sort-by.title')"
|
||||
/>
|
||||
<dropdown-menu
|
||||
v-model="selected_groupby_option_id"
|
||||
:options="groupby_options"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #heading-left>
|
||||
<p class="title is-4" v-text="composer.name" />
|
||||
</template>
|
||||
@@ -38,7 +54,7 @@
|
||||
"
|
||||
/>
|
||||
</p>
|
||||
<list-tracks :tracks="tracks.items" :expression="play_expression" />
|
||||
<list-tracks :tracks="tracks" :expression="expression" />
|
||||
<modal-dialog-composer
|
||||
:show="show_composer_details_modal"
|
||||
:composer="composer"
|
||||
@@ -51,9 +67,13 @@
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||
import IndexButtonList from '@/components/IndexButtonList.vue'
|
||||
import ListTracks from '@/components/ListTracks.vue'
|
||||
import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import { byName, byRating, GroupByList } from '@/lib/GroupByList'
|
||||
|
||||
const dataObject = {
|
||||
load: function (to) {
|
||||
@@ -65,7 +85,7 @@ const dataObject = {
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.composer = response[0].data
|
||||
vm.tracks = response[1].data.tracks
|
||||
vm.tracks_list = new GroupByList(response[1].data.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +93,8 @@ export default {
|
||||
name: 'PageComposerTracks',
|
||||
components: {
|
||||
ContentWithHeading,
|
||||
DropdownMenu,
|
||||
IndexButtonList,
|
||||
ListTracks,
|
||||
ModalDialogComposer
|
||||
},
|
||||
@@ -92,16 +114,44 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
tracks: { items: [] },
|
||||
groupby_options: [
|
||||
{
|
||||
id: 1,
|
||||
name: this.$t('page.composer.sort-by.name'),
|
||||
options: byName('title_sort')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: this.$t('page.composer.sort-by.rating'),
|
||||
options: byRating('rating', {
|
||||
direction: 'desc'
|
||||
})
|
||||
}
|
||||
],
|
||||
composer: {},
|
||||
|
||||
show_composer_details_modal: false
|
||||
show_composer_details_modal: false,
|
||||
tracks_list: new GroupByList()
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
play_expression() {
|
||||
expression() {
|
||||
return 'composer is "' + this.composer.name + '" and media_kind is music'
|
||||
},
|
||||
selected_groupby_option_id: {
|
||||
get() {
|
||||
return this.$store.state.composer_tracks_sort
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit(types.COMPOSER_TRACKS_SORT, value)
|
||||
}
|
||||
},
|
||||
tracks() {
|
||||
const groupBy = this.groupby_options.find(
|
||||
(o) => o.id === this.selected_groupby_option_id
|
||||
)
|
||||
this.tracks_list.group(groupBy.options)
|
||||
return this.tracks_list
|
||||
}
|
||||
},
|
||||
|
||||
@@ -115,7 +165,7 @@ export default {
|
||||
},
|
||||
|
||||
play: function () {
|
||||
webapi.player_play_expression(this.play_expression, true)
|
||||
webapi.player_play_expression(this.expression, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,20 @@
|
||||
<div>
|
||||
<content-with-heading>
|
||||
<template #options>
|
||||
<index-button-list :index="index_list" />
|
||||
<index-button-list :index="tracks.indexList" />
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<p
|
||||
class="heading"
|
||||
style="margin-bottom: 24px"
|
||||
v-text="$t('page.genre.sort-by.title')"
|
||||
/>
|
||||
<dropdown-menu
|
||||
v-model="selected_groupby_option_id"
|
||||
:options="groupby_options"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #heading-left>
|
||||
<p class="title is-4" v-text="genre.name" />
|
||||
@@ -35,7 +48,7 @@
|
||||
v-text="$t('page.genre.track-count', { count: genre.track_count })"
|
||||
/>
|
||||
</p>
|
||||
<list-tracks :tracks="tracks.items" :expression="expression" />
|
||||
<list-tracks :tracks="tracks" :expression="expression" />
|
||||
<modal-dialog-genre
|
||||
:show="show_genre_details_modal"
|
||||
:genre="genre"
|
||||
@@ -48,10 +61,13 @@
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||
import IndexButtonList from '@/components/IndexButtonList.vue'
|
||||
import ListTracks from '@/components/ListTracks.vue'
|
||||
import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
|
||||
import webapi from '@/webapi'
|
||||
import * as types from '@/store/mutation_types'
|
||||
import { byName, byRating, GroupByList } from '@/lib/GroupByList'
|
||||
|
||||
const dataObject = {
|
||||
load: function (to) {
|
||||
@@ -63,7 +79,7 @@ const dataObject = {
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.genre = response[0].data
|
||||
vm.tracks = response[1].data.tracks
|
||||
vm.tracks_list = new GroupByList(response[1].data.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +87,9 @@ export default {
|
||||
name: 'PageGenreTracks',
|
||||
components: {
|
||||
ContentWithHeading,
|
||||
ListTracks,
|
||||
DropdownMenu,
|
||||
IndexButtonList,
|
||||
ListTracks,
|
||||
ModalDialogGenre
|
||||
},
|
||||
|
||||
@@ -91,26 +108,44 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
tracks: { items: [] },
|
||||
genre: '',
|
||||
|
||||
show_genre_details_modal: false
|
||||
genre: {},
|
||||
groupby_options: [
|
||||
{
|
||||
id: 1,
|
||||
name: this.$t('page.genre.sort-by.name'),
|
||||
options: byName('title_sort')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: this.$t('page.genre.sort-by.rating'),
|
||||
options: byRating('rating', {
|
||||
direction: 'desc'
|
||||
})
|
||||
}
|
||||
],
|
||||
show_genre_details_modal: false,
|
||||
tracks_list: new GroupByList()
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
index_list() {
|
||||
return [
|
||||
...new Set(
|
||||
this.tracks.items.map((track) =>
|
||||
track.title_sort.charAt(0).toUpperCase()
|
||||
)
|
||||
)
|
||||
]
|
||||
},
|
||||
|
||||
expression() {
|
||||
return 'genre is "' + this.genre.name + '" and media_kind is music'
|
||||
},
|
||||
selected_groupby_option_id: {
|
||||
get() {
|
||||
return this.$store.state.genre_tracks_sort
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit(types.GENRE_TRACKS_SORT, value)
|
||||
}
|
||||
},
|
||||
tracks() {
|
||||
const groupBy = this.groupby_options.find(
|
||||
(o) => o.id === this.selected_groupby_option_id
|
||||
)
|
||||
this.tracks_list.group(groupBy.options)
|
||||
return this.tracks_list
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<template #content>
|
||||
<p
|
||||
class="heading has-text-centered-mobile"
|
||||
v-text="$t('page.playlist.length', { length: tracks.length })"
|
||||
v-text="$t('page.playlist.count', { count: tracks.count })"
|
||||
/>
|
||||
<list-tracks :tracks="tracks" :uris="uris" />
|
||||
<modal-dialog-playlist
|
||||
@@ -38,6 +38,7 @@ import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import ListTracks from '@/components/ListTracks.vue'
|
||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
|
||||
import webapi from '@/webapi'
|
||||
import { GroupByList } from '@/lib/GroupByList'
|
||||
|
||||
const dataObject = {
|
||||
load: function (to) {
|
||||
@@ -49,7 +50,7 @@ const dataObject = {
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.playlist = response[0].data
|
||||
vm.tracks = response[1].data.items
|
||||
vm.tracks = new GroupByList(response[1].data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +74,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
playlist: {},
|
||||
tracks: [],
|
||||
|
||||
tracks: new GroupByList(),
|
||||
show_playlist_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -61,6 +61,7 @@ import ListTracks from '@/components/ListTracks.vue'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
|
||||
import ModalDialog from '@/components/ModalDialog.vue'
|
||||
import webapi from '@/webapi'
|
||||
import { GroupByList } from '@/lib/GroupByList'
|
||||
|
||||
const dataObject = {
|
||||
load: function (to) {
|
||||
@@ -72,7 +73,7 @@ const dataObject = {
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.album = response[0].data
|
||||
vm.tracks = response[1].data.tracks.items
|
||||
vm.tracks = new GroupByList(response[1].data.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +102,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
album: {},
|
||||
tracks: [],
|
||||
|
||||
tracks: new GroupByList(),
|
||||
show_album_details_modal: false,
|
||||
|
||||
show_remove_podcast_modal: false,
|
||||
rss_playlist_to_remove: {}
|
||||
}
|
||||
@@ -112,7 +111,7 @@ export default {
|
||||
|
||||
computed: {
|
||||
new_tracks() {
|
||||
return this.tracks.filter((track) => track.play_count === 0).length
|
||||
return this.tracks.items.filter((track) => track.play_count === 0).length
|
||||
}
|
||||
},
|
||||
|
||||
@@ -149,7 +148,7 @@ export default {
|
||||
|
||||
reload_tracks: function () {
|
||||
webapi.library_podcast_episodes(this.album.id).then(({ data }) => {
|
||||
this.tracks = data.tracks.items
|
||||
this.tracks = new GroupByList(data.tracks)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
<template #content>
|
||||
<list-tracks
|
||||
:tracks="new_episodes.items"
|
||||
:tracks="new_episodes"
|
||||
:show_progress="true"
|
||||
@play-count-changed="reload_new_episodes"
|
||||
/>
|
||||
@@ -75,7 +75,7 @@ const dataObject = {
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.albums = new GroupByList(response[0].data)
|
||||
vm.new_episodes = response[1].data.tracks
|
||||
vm.new_episodes = new GroupByList(response[1].data.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ export default {
|
||||
|
||||
reload_new_episodes: function () {
|
||||
webapi.library_podcasts_new_episodes().then(({ data }) => {
|
||||
this.new_episodes = data.tracks
|
||||
this.new_episodes = new GroupByList(data.tracks)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
class="heading has-text-centered-mobile"
|
||||
v-text="$t('page.radio.count', { count: tracks.total })"
|
||||
/>
|
||||
<list-tracks :tracks="tracks.items" />
|
||||
<list-tracks :tracks="tracks" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</div>
|
||||
@@ -19,6 +19,7 @@
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import ListTracks from '@/components/ListTracks.vue'
|
||||
import webapi from '@/webapi'
|
||||
import { GroupByList } from '../lib/GroupByList'
|
||||
|
||||
const dataObject = {
|
||||
load: function (to) {
|
||||
@@ -26,7 +27,7 @@ const dataObject = {
|
||||
},
|
||||
|
||||
set: function (vm, response) {
|
||||
vm.tracks = response.data.tracks
|
||||
vm.tracks = new GroupByList(response.data.tracks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
tracks: { items: [] }
|
||||
tracks: new GroupByList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<p class="title is-4" v-text="$t('page.search.tracks')" />
|
||||
</template>
|
||||
<template #content>
|
||||
<list-tracks :tracks="tracks.items" />
|
||||
<list-tracks :tracks="tracks" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<nav v-if="show_all_tracks_button" class="level">
|
||||
@@ -275,7 +275,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
search_query: '',
|
||||
tracks: { items: [], total: 0 },
|
||||
tracks: new GroupByList(),
|
||||
artists: new GroupByList(),
|
||||
albums: new GroupByList(),
|
||||
composers: new GroupByList(),
|
||||
@@ -407,7 +407,7 @@ export default {
|
||||
}
|
||||
|
||||
webapi.search(searchParams).then(({ data }) => {
|
||||
this.tracks = data.tracks ? data.tracks : { items: [], total: 0 }
|
||||
this.tracks = new GroupByList(data.tracks)
|
||||
this.artists = new GroupByList(data.artists)
|
||||
this.albums = new GroupByList(data.albums)
|
||||
this.composers = new GroupByList(data.composers)
|
||||
|
||||
Reference in New Issue
Block a user