mirror of
https://github.com/owntone/owntone-server.git
synced 2025-04-19 02:15:18 -04:00
[web-src] Index based navigation for artists, albums, genres
This commit is contained in:
parent
a1a8864270
commit
95b1cfc70b
29
web-src/src/components/IndexButtonList.vue
Normal file
29
web-src/src/components/IndexButtonList.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div class="buttons is-centered fd-is-square">
|
||||||
|
<a v-for="char in filtered_index" :key="char" class="button is-small" @click="nav(char)">{{ char }}</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'IndexButtonList',
|
||||||
|
|
||||||
|
props: ['index'],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
filtered_index () {
|
||||||
|
const specialChars = '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~'
|
||||||
|
return this.index.filter(c => !specialChars.includes(c))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
nav: function (id) {
|
||||||
|
this.$router.push({ path: this.$router.currentRoute.path + '#index_' + id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="media">
|
<div class="media" :id="'index_' + anchor">
|
||||||
<div class="media-content fd-has-action is-clipped" v-on:click="open_album">
|
<div class="media-content fd-has-action is-clipped" v-on:click="open_album">
|
||||||
<h1 class="title is-6">{{ album.name }}</h1>
|
<h1 class="title is-6">{{ album.name }}</h1>
|
||||||
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artist }}</b></h2>
|
<h2 class="subtitle is-7 has-text-grey"><b>{{ album.artist }}</b></h2>
|
||||||
@ -56,7 +56,7 @@ export default {
|
|||||||
name: 'ListItemAlbum',
|
name: 'ListItemAlbum',
|
||||||
components: { ModalDialog },
|
components: { ModalDialog },
|
||||||
|
|
||||||
props: ['album', 'media_kind'],
|
props: ['album', 'media_kind', 'anchor'],
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="media">
|
<div class="media" :id="'index_' + anchor">
|
||||||
<div class="media-content fd-has-action is-clipped" v-on:click="open_artist">
|
<div class="media-content fd-has-action is-clipped" v-on:click="open_artist">
|
||||||
<h1 class="title is-6">{{ artist.name }}</h1>
|
<h1 class="title is-6">{{ artist.name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -51,7 +51,7 @@ export default {
|
|||||||
name: 'PartArtist',
|
name: 'PartArtist',
|
||||||
components: { ModalDialog },
|
components: { ModalDialog },
|
||||||
|
|
||||||
props: ['artist'],
|
props: ['artist', 'anchor'],
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="media">
|
<div class="media" :id="'index_' + anchor">
|
||||||
<div class="media-content fd-has-action is-clipped" v-on:click="open_genre">
|
<div class="media-content fd-has-action is-clipped" v-on:click="open_genre">
|
||||||
<h1 class="title is-6">{{ genre.name }}</h1>
|
<h1 class="title is-6">{{ genre.name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -53,7 +53,7 @@ export default {
|
|||||||
name: 'PartGenre',
|
name: 'PartGenre',
|
||||||
components: { ModalDialog },
|
components: { ModalDialog },
|
||||||
|
|
||||||
props: [ 'genre' ],
|
props: [ 'genre', 'anchor' ],
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="section fd-tabs-section" v-if="spotify_enabled">
|
<section class="section fd-remove-padding-bottom" v-if="spotify_enabled">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<div class="column is-four-fifths">
|
<div class="column is-four-fifths">
|
||||||
|
@ -38,11 +38,20 @@ a.navbar-item {
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fd-remove-padding-bottom {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.fd-has-padding-left-right {
|
.fd-has-padding-left-right {
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
padding-right: 24px;
|
padding-right: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fd-is-square .button {
|
||||||
|
height: 27px;
|
||||||
|
width: 27px;
|
||||||
|
}
|
||||||
|
|
||||||
.fd-is-text-clipped {
|
.fd-is-text-clipped {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -65,7 +74,17 @@ a.navbar-item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fd-tabs-section {
|
.fd-tabs-section {
|
||||||
padding-bottom: 0;
|
padding-bottom: 3px;
|
||||||
|
padding-top: 3px;
|
||||||
|
background: white;
|
||||||
|
top: 3.25rem;
|
||||||
|
z-index: 20;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.fd-tabs-section + section.fd-content {
|
||||||
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fd-progress-bar {
|
.fd-progress-bar {
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
<tabs-music></tabs-music>
|
<tabs-music></tabs-music>
|
||||||
|
|
||||||
<content-with-heading>
|
<content-with-heading>
|
||||||
|
<template slot="options">
|
||||||
|
<index-button-list :index="index_list"></index-button-list>
|
||||||
|
</template>
|
||||||
<template slot="heading-left">
|
<template slot="heading-left">
|
||||||
<p class="title is-4">Albums</p>
|
<p class="title is-4">Albums</p>
|
||||||
<p class="heading">{{ albums.total }} albums</p>
|
<p class="heading">{{ albums.total }} albums</p>
|
||||||
@ -16,7 +19,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-album v-for="album in albums.items" :key="album.id" :album="album" v-if="!hide_singles || album.track_count > 2"></list-item-album>
|
<list-item-album v-for="(album, index) in albums.items" :key="album.id" :album="album" :anchor="anchor(album, index)" v-if="!hide_singles || album.track_count > 2"></list-item-album>
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -26,6 +29,7 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsMusic from '@/components/TabsMusic'
|
||||||
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
@ -43,23 +47,33 @@ const albumsData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageAlbums',
|
name: 'PageAlbums',
|
||||||
mixins: [ LoadDataBeforeEnterMixin(albumsData) ],
|
mixins: [ LoadDataBeforeEnterMixin(albumsData) ],
|
||||||
components: { ContentWithHeading, TabsMusic, ListItemAlbum },
|
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemAlbum },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
albums: {}
|
albums: { items: [] }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hide_singles () {
|
hide_singles () {
|
||||||
return this.$store.state.hide_singles
|
return this.$store.state.hide_singles
|
||||||
|
},
|
||||||
|
|
||||||
|
index_list () {
|
||||||
|
return [...new Set(this.albums.items
|
||||||
|
.filter(album => !this.$store.state.hide_singles || album.track_count > 2)
|
||||||
|
.map(album => album.name_sort.charAt(0).toUpperCase()))]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update_hide_singles: function (e) {
|
update_hide_singles: function (e) {
|
||||||
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
|
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
|
||||||
|
},
|
||||||
|
|
||||||
|
anchor: function (album, index) {
|
||||||
|
return album.name_sort.charAt(0).toUpperCase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
<tabs-music></tabs-music>
|
<tabs-music></tabs-music>
|
||||||
|
|
||||||
<content-with-heading>
|
<content-with-heading>
|
||||||
|
<template slot="options">
|
||||||
|
<index-button-list :index="index_list"></index-button-list>
|
||||||
|
</template>
|
||||||
<template slot="heading-left">
|
<template slot="heading-left">
|
||||||
<p class="title is-4">Artists</p>
|
<p class="title is-4">Artists</p>
|
||||||
<p class="heading">{{ artists.total }} artists</p>
|
<p class="heading">{{ artists.total }} artists</p>
|
||||||
@ -16,7 +19,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-artist v-for="artist in artists.items" :key="artist.id" :artist="artist" v-if="!hide_singles || artist.track_count > (artist.album_count * 2)"></list-item-artist>
|
<list-item-artist v-for="(artist, index) in artists.items" :key="artist.id" :artist="artist" :anchor="anchor(artist, index)" v-if="!hide_singles || artist.track_count > (artist.album_count * 2)"></list-item-artist>
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -26,6 +29,7 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsMusic from '@/components/TabsMusic'
|
||||||
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
import ListItemArtist from '@/components/ListItemArtist'
|
import ListItemArtist from '@/components/ListItemArtist'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
import * as types from '@/store/mutation_types'
|
import * as types from '@/store/mutation_types'
|
||||||
@ -43,23 +47,33 @@ const artistsData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageArtists',
|
name: 'PageArtists',
|
||||||
mixins: [ LoadDataBeforeEnterMixin(artistsData) ],
|
mixins: [ LoadDataBeforeEnterMixin(artistsData) ],
|
||||||
components: { ContentWithHeading, TabsMusic, ListItemArtist },
|
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemArtist },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
artists: {}
|
artists: { items: [] }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
hide_singles () {
|
hide_singles () {
|
||||||
return this.$store.state.hide_singles
|
return this.$store.state.hide_singles
|
||||||
|
},
|
||||||
|
|
||||||
|
index_list () {
|
||||||
|
return [...new Set(this.artists.items
|
||||||
|
.filter(artist => !this.$store.state.hide_singles || artist.track_count > (artist.album_count * 2))
|
||||||
|
.map(artist => artist.name_sort.charAt(0).toUpperCase()))]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update_hide_singles: function (e) {
|
update_hide_singles: function (e) {
|
||||||
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
|
this.$store.commit(types.HIDE_SINGLES, !this.hide_singles)
|
||||||
|
},
|
||||||
|
|
||||||
|
anchor: function (artist, index) {
|
||||||
|
return artist.name_sort.charAt(0).toUpperCase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,15 @@
|
|||||||
<tabs-music></tabs-music>
|
<tabs-music></tabs-music>
|
||||||
|
|
||||||
<content-with-heading>
|
<content-with-heading>
|
||||||
|
<template slot="options">
|
||||||
|
<index-button-list :index="index_list"></index-button-list>
|
||||||
|
</template>
|
||||||
<template slot="heading-left">
|
<template slot="heading-left">
|
||||||
<p class="title is-4">Genres</p>
|
<p class="title is-4">Genres</p>
|
||||||
<p class="heading">{{ genres.total }} genres</p>
|
<p class="heading">{{ genres.total }} genres</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<list-item-genre v-for="genre in genres.items" :key="genre.name" :genre="genre"></list-item-genre>
|
<list-item-genre v-for="(genre, index) in genres.items" :key="genre.name" :genre="genre" :anchor="anchor(genre, index)"></list-item-genre>
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
</div>
|
</div>
|
||||||
@ -18,6 +21,7 @@
|
|||||||
import { LoadDataBeforeEnterMixin } from './mixin'
|
import { LoadDataBeforeEnterMixin } from './mixin'
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||||
import TabsMusic from '@/components/TabsMusic'
|
import TabsMusic from '@/components/TabsMusic'
|
||||||
|
import IndexButtonList from '@/components/IndexButtonList'
|
||||||
import ListItemGenre from '@/components/ListItemGenre'
|
import ListItemGenre from '@/components/ListItemGenre'
|
||||||
import webapi from '@/webapi'
|
import webapi from '@/webapi'
|
||||||
|
|
||||||
@ -34,15 +38,25 @@ const genresData = {
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageGenres',
|
name: 'PageGenres',
|
||||||
mixins: [ LoadDataBeforeEnterMixin(genresData) ],
|
mixins: [ LoadDataBeforeEnterMixin(genresData) ],
|
||||||
components: { ContentWithHeading, TabsMusic, ListItemGenre },
|
components: { ContentWithHeading, TabsMusic, IndexButtonList, ListItemGenre },
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
genres: {}
|
genres: { items: [] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
index_list () {
|
||||||
|
return [...new Set(this.genres.items
|
||||||
|
.map(genre => genre.name.charAt(0).toUpperCase()))]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
anchor: function (genre, index) {
|
||||||
|
return genre.name.charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Search field + recent searches -->
|
<!-- Search field + recent searches -->
|
||||||
<section class="section fd-tabs-section">
|
<section class="section fd-remove-padding-bottom">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<div class="column is-four-fifths">
|
<div class="column is-four-fifths">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- Search field + recent searches -->
|
<!-- Search field + recent searches -->
|
||||||
<section class="section fd-tabs-section">
|
<section class="section fd-remove-padding-bottom">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<div class="column is-four-fifths">
|
<div class="column is-four-fifths">
|
||||||
|
@ -75,7 +75,7 @@ export const router = new VueRouter({
|
|||||||
path: '/music/artists',
|
path: '/music/artists',
|
||||||
name: 'Artists',
|
name: 'Artists',
|
||||||
component: PageArtists,
|
component: PageArtists,
|
||||||
meta: { show_progress: true }
|
meta: { show_progress: true, has_index: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/music/artists/:artist_id',
|
path: '/music/artists/:artist_id',
|
||||||
@ -93,7 +93,7 @@ export const router = new VueRouter({
|
|||||||
path: '/music/albums',
|
path: '/music/albums',
|
||||||
name: 'Albums',
|
name: 'Albums',
|
||||||
component: PageAlbums,
|
component: PageAlbums,
|
||||||
meta: { show_progress: true }
|
meta: { show_progress: true, has_index: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/music/albums/:album_id',
|
path: '/music/albums/:album_id',
|
||||||
@ -105,7 +105,7 @@ export const router = new VueRouter({
|
|||||||
path: '/music/genres',
|
path: '/music/genres',
|
||||||
name: 'Genres',
|
name: 'Genres',
|
||||||
component: PageGenres,
|
component: PageGenres,
|
||||||
meta: { show_progress: true }
|
meta: { show_progress: true, has_index: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/music/genres/:genre',
|
path: '/music/genres/:genre',
|
||||||
@ -201,12 +201,27 @@ export const router = new VueRouter({
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
scrollBehavior (to, from, savedPosition) {
|
scrollBehavior (to, from, savedPosition) {
|
||||||
|
// console.log(to.path + '_' + from.path + '__' + to.hash + ' savedPosition:' + savedPosition)
|
||||||
if (savedPosition) {
|
if (savedPosition) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve(savedPosition)
|
resolve(savedPosition)
|
||||||
}, 500)
|
}, 500)
|
||||||
})
|
})
|
||||||
|
} else if (to.path === from.path && to.hash) {
|
||||||
|
return { selector: to.hash, offset: { x: 0, y: 90 } }
|
||||||
|
} else if (to.hash) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ selector: to.hash, offset: { x: 0, y: 90 } })
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
} else if (to.meta.has_index) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ selector: '#top', offset: { x: 0, y: 110 } })
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
return { x: 0, y: 0 }
|
return { x: 0, y: 0 }
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="section">
|
<section class="section fd-content">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<div class="column is-four-fifths">
|
<div class="column is-four-fifths">
|
||||||
<nav class="level">
|
<slot name="options"></slot>
|
||||||
|
<nav class="level" id="top">
|
||||||
<!-- Left side -->
|
<!-- Left side -->
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<div class="level-item has-text-centered-mobile">
|
<div class="level-item has-text-centered-mobile">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user