kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
				
				
				
			Resolve "UX, UI : Search Page"
							rodzic
							
								
									88b04886d1
								
							
						
					
					
						commit
						667d9d2ba6
					
				| 
						 | 
				
			
			@ -1,14 +1,23 @@
 | 
			
		|||
import django_filters
 | 
			
		||||
 | 
			
		||||
from django_filters import rest_framework as filters
 | 
			
		||||
 | 
			
		||||
from funkwhale_api.common import filters as common_filters
 | 
			
		||||
from funkwhale_api.music import utils
 | 
			
		||||
 | 
			
		||||
from . import models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RadioFilter(django_filters.FilterSet):
 | 
			
		||||
    scope = common_filters.ActorScopeFilter(actor_field="user__actor", distinct=True)
 | 
			
		||||
    q = filters.CharFilter(field_name="_", method="filter_q")
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = models.Radio
 | 
			
		||||
        fields = {
 | 
			
		||||
            "name": ["exact", "iexact", "startswith", "icontains"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def filter_q(self, queryset, name, value):
 | 
			
		||||
        query = utils.get_query(value, ["name", "user__username"])
 | 
			
		||||
        return queryset.filter(query)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
Dedicated, advanced search page (#370)
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +42,7 @@ export default {
 | 
			
		|||
        // Cancel any API search request to backend…
 | 
			
		||||
        jQuery(this.$el).search('cancel query');
 | 
			
		||||
        // Go direct to the artist page…
 | 
			
		||||
        router.push("/library/artists?query=" + searchQuery + "&page=1&paginateBy=25&ordering=name");
 | 
			
		||||
        router.push(`/search?q=${searchQuery}&type=artists`);
 | 
			
		||||
	}
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -147,8 +147,12 @@ export default {
 | 
			
		|||
              },
 | 
			
		||||
              getId (t) {
 | 
			
		||||
                return t.name
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              code: 'more',
 | 
			
		||||
              name: '',
 | 
			
		||||
            },
 | 
			
		||||
          ]
 | 
			
		||||
          categories.forEach(category => {
 | 
			
		||||
            results[category.code] = {
 | 
			
		||||
| 
						 | 
				
			
			@ -193,6 +197,22 @@ export default {
 | 
			
		|||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            else if (category.code === 'more') {
 | 
			
		||||
              let searchMessage = self.$pgettext('Search/*/*', 'More results 🡒')
 | 
			
		||||
              results['more'] = {
 | 
			
		||||
                name: '',
 | 
			
		||||
                results: [{
 | 
			
		||||
                  title: searchMessage,
 | 
			
		||||
                  routerUrl: {
 | 
			
		||||
                    name: 'search',
 | 
			
		||||
                    query: {
 | 
			
		||||
                      type: "artists",
 | 
			
		||||
                      q: searchQuery
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }]
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
              initialResponse[category.code].forEach(result => {
 | 
			
		||||
                isEmptyResults = false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,7 +91,9 @@ export default new Router({
 | 
			
		|||
        import(/* webpackChunkName: "core" */ "@/views/Search"),
 | 
			
		||||
      props: route => ({
 | 
			
		||||
        initialId: route.query.id,
 | 
			
		||||
        type: route.query.type,
 | 
			
		||||
        initialType: route.query.type || 'artists',
 | 
			
		||||
        initialQuery: route.query.q,
 | 
			
		||||
        initialPage: parseInt(route.query.page) || 1,
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,7 @@ $bottom-player-height: 4rem;
 | 
			
		|||
@import "./components/_playlist_editor.scss";
 | 
			
		||||
@import "./components/_queue.scss";
 | 
			
		||||
@import "./components/_settings_group.scss";
 | 
			
		||||
@import "./components/_search.scss";
 | 
			
		||||
@import "./components/_sidebar.scss";
 | 
			
		||||
@import "./components/_table.scss";
 | 
			
		||||
@import "./components/_tags_list.scss";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
.search-wrapper .category.search .results {
 | 
			
		||||
  .category:last-child {
 | 
			
		||||
    .results {
 | 
			
		||||
      border-left: 0;
 | 
			
		||||
      text-align: right;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +1,83 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <main class="main pusher" v-title="labels.title">
 | 
			
		||||
    <section class="ui vertical stripe segment">
 | 
			
		||||
      <div class="ui small text container">
 | 
			
		||||
      <div class="ui small text container" v-if="initialId">
 | 
			
		||||
        <h2>{{ labels.title }}</h2>
 | 
			
		||||
        <remote-search-form :initial-id="initialId" :type="type"></remote-search-form>
 | 
			
		||||
        <remote-search-form :initial-id="initialId" :type="initialType"></remote-search-form>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="ui container" v-else>
 | 
			
		||||
        <h2>
 | 
			
		||||
          <label for="query">
 | 
			
		||||
            <translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
 | 
			
		||||
          </label>
 | 
			
		||||
        </h2>
 | 
			
		||||
        <form class="ui form" @submit.prevent="page = 1; search()">
 | 
			
		||||
          <div class="ui field">
 | 
			
		||||
            <div class="ui action input">
 | 
			
		||||
              <input class="ui input" id="query" name="query" type="text" v-model="query">
 | 
			
		||||
              <button type="submit" class="ui icon button">
 | 
			
		||||
                <i class="search icon"></i>
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </form>
 | 
			
		||||
        <div class="ui secondary pointing menu">
 | 
			
		||||
          <a
 | 
			
		||||
            :class="['item', {active: type === t.id}]" 
 | 
			
		||||
            @click.prevent="type = t.id"
 | 
			
		||||
            v-for="t in types"
 | 
			
		||||
            href=""
 | 
			
		||||
            :key="t.id">
 | 
			
		||||
            {{ t.label }}
 | 
			
		||||
            <span
 | 
			
		||||
              v-if="results[t.id]"
 | 
			
		||||
              class="ui circular mini right floated label">
 | 
			
		||||
              {{ results[t.id].count }}</span>
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="isLoading" >
 | 
			
		||||
          <div v-if="isLoading" class="ui inverted active dimmer">
 | 
			
		||||
            <div class="ui loader"></div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    
 | 
			
		||||
        <empty-state v-else-if="!currentResults || currentResults.count === 0" @refresh="search" :refresh="true"></empty-state>
 | 
			
		||||
        
 | 
			
		||||
        <div v-else-if="type === 'artists'" class="ui five app-cards cards">
 | 
			
		||||
          <artist-card :artist="artist" v-for="artist in currentResults.results" :key="artist.id"></artist-card>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div v-else-if="type === 'albums'" class="ui five app-cards cards">
 | 
			
		||||
          <album-card
 | 
			
		||||
            v-for="album in currentResults.results"
 | 
			
		||||
            :key="album.id"
 | 
			
		||||
            :album="album"></album-card>        
 | 
			
		||||
        </div>
 | 
			
		||||
        <track-table v-else-if="type === 'tracks'" :tracks="currentResults.results"></track-table>    
 | 
			
		||||
        <playlist-card-list v-else-if="type === 'playlists'"  :playlists="currentResults.results"></playlist-card-list>
 | 
			
		||||
        <div
 | 
			
		||||
          v-else-if="type === 'radios'"
 | 
			
		||||
          class="ui cards">
 | 
			
		||||
          <radio-card
 | 
			
		||||
            type="custom"
 | 
			
		||||
            v-for="radio in currentResults.results"
 | 
			
		||||
            :key="radio.id"
 | 
			
		||||
            :custom-radio="radio"></radio-card>
 | 
			
		||||
        </div>
 | 
			
		||||
        <tags-list
 | 
			
		||||
          v-else-if="type === 'tags'"
 | 
			
		||||
          :truncate-size="200"
 | 
			
		||||
          :limit="paginateBy"
 | 
			
		||||
          :tags="currentResults.results.map(t => {return t.name })"></tags-list>
 | 
			
		||||
      
 | 
			
		||||
        <pagination
 | 
			
		||||
          v-if="currentResults && currentResults.count > paginateBy"
 | 
			
		||||
          @page-changed="page = $event"
 | 
			
		||||
          :current="page"
 | 
			
		||||
          :paginate-by="paginateBy"
 | 
			
		||||
          :total="currentResults.count"
 | 
			
		||||
          ></pagination>
 | 
			
		||||
      
 | 
			
		||||
      </div>
 | 
			
		||||
    </section>
 | 
			
		||||
  </main>
 | 
			
		||||
| 
						 | 
				
			
			@ -11,25 +85,151 @@
 | 
			
		|||
 | 
			
		||||
<script>
 | 
			
		||||
import RemoteSearchForm from '@/components/RemoteSearchForm'
 | 
			
		||||
import ArtistCard from "@/components/audio/artist/Card"
 | 
			
		||||
import AlbumCard from "@/components/audio/album/Card"
 | 
			
		||||
import TrackTable from "@/components/audio/track/Table"
 | 
			
		||||
import Pagination from '@/components/Pagination'
 | 
			
		||||
import PlaylistCardList from "@/components/playlists/CardList"
 | 
			
		||||
import RadioCard from "@/components/radios/Card"
 | 
			
		||||
import TagsList from "@/components/tags/List"
 | 
			
		||||
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    initialId: { type: String, required: false},
 | 
			
		||||
    type: { type: String, required: false},
 | 
			
		||||
    initialType: { type: String, required: false},
 | 
			
		||||
    initialQuery: { type: String, required: false},
 | 
			
		||||
    initialPage: { type: Number, required: false},
 | 
			
		||||
  },
 | 
			
		||||
  components: {
 | 
			
		||||
    RemoteSearchForm,
 | 
			
		||||
    ArtistCard,
 | 
			
		||||
    AlbumCard,
 | 
			
		||||
    TrackTable,
 | 
			
		||||
    Pagination,
 | 
			
		||||
    PlaylistCardList,
 | 
			
		||||
    RadioCard,
 | 
			
		||||
    TagsList,
 | 
			
		||||
  },
 | 
			
		||||
  data () {
 | 
			
		||||
    return {
 | 
			
		||||
      query: this.initialQuery,
 | 
			
		||||
      type: this.initialType,
 | 
			
		||||
      page: this.initialPage,
 | 
			
		||||
      results: {
 | 
			
		||||
        artists: null,
 | 
			
		||||
        albums: null,
 | 
			
		||||
        tracks: null,
 | 
			
		||||
        playlists: null,
 | 
			
		||||
        radios: null,
 | 
			
		||||
        tags: null,
 | 
			
		||||
      },
 | 
			
		||||
      isLoading: false,
 | 
			
		||||
      paginateBy: 25,
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  created () {
 | 
			
		||||
    this.search()
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    labels() {
 | 
			
		||||
      let title = this.$pgettext('Head/Fetch/Title', "Search a remote object")
 | 
			
		||||
      if (this.type === "rss") {
 | 
			
		||||
        title = this.$pgettext('Head/Fetch/Title', "Subscribe to a podcast RSS feed")
 | 
			
		||||
      }
 | 
			
		||||
      let title = this.$pgettext("Content/Search/Input.Label/Noun", "Search")
 | 
			
		||||
      if (this.initialId) {
 | 
			
		||||
        title = this.$pgettext('Head/Fetch/Title', "Search a remote object")
 | 
			
		||||
        if (this.type === "rss") {
 | 
			
		||||
          title = this.$pgettext('Head/Fetch/Title', "Subscribe to a podcast RSS feed")
 | 
			
		||||
        }
 | 
			
		||||
      } 
 | 
			
		||||
      return {
 | 
			
		||||
        title,
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    types () {
 | 
			
		||||
      return [
 | 
			
		||||
        {
 | 
			
		||||
          id: 'artists',
 | 
			
		||||
          label: this.$pgettext("*/*/*/Noun", "Artists"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 'albums',
 | 
			
		||||
          label: this.$pgettext("*/*/*", "Albums"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 'tracks',
 | 
			
		||||
          label: this.$pgettext("*/*/*", "Tracks"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 'playlists',
 | 
			
		||||
          label: this.$pgettext("*/*/*", "Playlists"),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 'radios',
 | 
			
		||||
          label: this.$pgettext("*/*/*", "Radios"),
 | 
			
		||||
          endpoint: 'radios/radios',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          id: 'tags',
 | 
			
		||||
          label: this.$pgettext("*/*/*", "Tags"),
 | 
			
		||||
        },
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    currentType () {
 | 
			
		||||
      return this.types.filter(t => {
 | 
			
		||||
        return t.id === this.type
 | 
			
		||||
      })[0]
 | 
			
		||||
    },
 | 
			
		||||
    currentResults () {
 | 
			
		||||
      return this.results[this.currentType.id]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async search () {
 | 
			
		||||
      this.updateQueryString()
 | 
			
		||||
      if (!this.query) {
 | 
			
		||||
        this.types.forEach(t => {
 | 
			
		||||
          this.results[t.id] = null
 | 
			
		||||
        })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.isLoading = true
 | 
			
		||||
      let response = await axios.get(
 | 
			
		||||
        this.currentType.endpoint || this.currentType.id,
 | 
			
		||||
        {params: {q: this.query, page: this.page, page_size: this.paginateBy}}
 | 
			
		||||
      )
 | 
			
		||||
      this.results[this.currentType.id] = response.data
 | 
			
		||||
      this.isLoading = false
 | 
			
		||||
      this.types.forEach(t => {
 | 
			
		||||
        if (t.id != this.currentType.id) {
 | 
			
		||||
          axios.get(t.endpoint || t.id, {params: {q: this.query, page_size: 1}}).then(response => {
 | 
			
		||||
            this.results[t.id] = response.data
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    updateQueryString: function() {
 | 
			
		||||
      history.pushState(
 | 
			
		||||
        {},
 | 
			
		||||
        null,
 | 
			
		||||
        this.$route.path + '?' + new URLSearchParams(
 | 
			
		||||
          {
 | 
			
		||||
          q: this.query,
 | 
			
		||||
          page: this.page,
 | 
			
		||||
          type: this.type,
 | 
			
		||||
        }).toString()
 | 
			
		||||
      )
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    async type () {
 | 
			
		||||
      this.page = 1
 | 
			
		||||
      this.updateQueryString()
 | 
			
		||||
      await this.search()
 | 
			
		||||
    },
 | 
			
		||||
    async page () {
 | 
			
		||||
      this.updateQueryString()
 | 
			
		||||
      await this.search()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue