Feat(front): implement new search process
Co-Authored-By: ArneBo <arne@ecobasa.org> Co-Authored-By: Flupsi <upsiflu@gmail.com> Co-Authored-By: jon r <jon@allmende.io>
This commit is contained in:
		
							parent
							
								
									0026b6b741
								
							
						
					
					
						commit
						fb36fb2266
					
				| 
						 | 
				
			
			@ -0,0 +1,528 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import type { paths } from '~/generated/types.ts'
 | 
			
		||||
import type { RadioConfig } from '~/store/radios'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import { ref, watch, computed } from 'vue'
 | 
			
		||||
import { refDebounced } from '@vueuse/core'
 | 
			
		||||
import { trim, uniqBy } from 'lodash-es'
 | 
			
		||||
 | 
			
		||||
import useErrorHandler from '~/composables/useErrorHandler'
 | 
			
		||||
import { useI18n } from 'vue-i18n'
 | 
			
		||||
import { useModal } from '~/ui/composables/useModal.ts'
 | 
			
		||||
 | 
			
		||||
import ArtistCard from '~/components/artist/Card.vue'
 | 
			
		||||
import PlaylistCard from '~/components/playlists/Card.vue'
 | 
			
		||||
import TrackTable from '~/components/audio/track/Table.vue'
 | 
			
		||||
import AlbumCard from '~/components/album/Card.vue'
 | 
			
		||||
import RadioCard from '~/components/radios/Card.vue'
 | 
			
		||||
import RadioButton from '~/components/radios/Button.vue'
 | 
			
		||||
import TagsList from '~/components/tags/List.vue'
 | 
			
		||||
 | 
			
		||||
import Modal from '~/components/ui/Modal.vue'
 | 
			
		||||
import Spacer from '~/components/ui/Spacer.vue'
 | 
			
		||||
import Input from '~/components/ui/Input.vue'
 | 
			
		||||
import Section from '~/components/ui/Section.vue'
 | 
			
		||||
import Link from '~/components/ui/Link.vue'
 | 
			
		||||
import Loader from '~/components/ui/Loader.vue'
 | 
			
		||||
import Alert from '~/components/ui/Alert.vue'
 | 
			
		||||
import EmptyState from '~/components/common/EmptyState.vue'
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n()
 | 
			
		||||
 | 
			
		||||
const { isOpen, value: query } = useModal(
 | 
			
		||||
  'search', {
 | 
			
		||||
    on: () => '',
 | 
			
		||||
    isOn: (value) => value !== undefined && value !== ''
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// TODO:
 | 
			
		||||
// - Limit search results to 4
 | 
			
		||||
// - Add Link to specific search pages in each section where it applies
 | 
			
		||||
// - Read out the count from `result` (instead of the max. 4 visible results)
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 | 
			
		||||
- Categories (an Array of all categories) <- static configuration
 | 
			
		||||
  |
 | 
			
		||||
  | filter according to search query
 | 
			
		||||
  v
 | 
			
		||||
- Available Categories (also an Array of category configs)
 | 
			
		||||
  |
 | 
			
		||||
  | make sure that `open sections` is a
 | 
			
		||||
  | subset of available category types
 | 
			
		||||
  |
 | 
			
		||||
* Open Sections (a Set of category types)    <- user can expand/collapse sections
 | 
			
		||||
  |                                          <- new results can also open a category:
 | 
			
		||||
  |                                             if all were closed or none had results
 | 
			
		||||
  v
 | 
			
		||||
- Open Categories (this value is just computed, based on open sections)
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
// Search query
 | 
			
		||||
 | 
			
		||||
const queryDebounced = refDebounced(query, 500)
 | 
			
		||||
const trimmedQuery = computed(() => trim(trim(queryDebounced.value), '@'))
 | 
			
		||||
const isFetch = computed(() => ((trimmedQuery.value.startsWith('http://') || trimmedQuery.value.startsWith('https://')) || trimmedQuery.value.includes('@')) && !isRss.value)
 | 
			
		||||
const isRss = computed(() => trimmedQuery.value.includes('.rss') || trimmedQuery.value.includes('.xml'))
 | 
			
		||||
 | 
			
		||||
const isLoading = ref(false)
 | 
			
		||||
 | 
			
		||||
// Filter
 | 
			
		||||
 | 
			
		||||
type Category = 'artists' | 'albums' | 'tracks' | 'playlists' | 'tags' | 'radios' | 'podcasts' | 'series' | 'rss' | 'federation'
 | 
			
		||||
 | 
			
		||||
type SearchResponse = paths['/api/v2/search']['get']['responses']['200']['content']['application/json']
 | 
			
		||||
 | 
			
		||||
type Response = {
 | 
			
		||||
  artists: SearchResponse,
 | 
			
		||||
  albums: SearchResponse,
 | 
			
		||||
  tracks: SearchResponse,
 | 
			
		||||
  tags: SearchResponse,
 | 
			
		||||
  playlists: paths['/api/v2/playlists/']['get']['responses']['200']['content']['application/json'],
 | 
			
		||||
  radios: paths['/api/v2/radios/radios/']['get']['responses']['200']['content']['application/json'],
 | 
			
		||||
  podcasts: paths['/api/v2/artists/']['get']['responses']['200']['content']['application/json'],
 | 
			
		||||
  series: paths['/api/v2/albums/']['get']['responses']['200']['content']['application/json'],
 | 
			
		||||
  rss: paths['/api/v2/channels/rss-subscribe/']['post']['responses']['200']['content']['application/json'],
 | 
			
		||||
  federation: paths['/api/v2/federation/fetches/']['post']['responses']['201']['content']['application/json']
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Note that `federation` is a singleton list so that each result is a list */
 | 
			
		||||
type Results = {
 | 
			
		||||
  artists: SearchResponse['artists'],
 | 
			
		||||
  albums: SearchResponse['albums'],
 | 
			
		||||
  tracks: SearchResponse['tracks'],
 | 
			
		||||
  tags: SearchResponse['tags'],
 | 
			
		||||
  playlists: Response['playlists']['results'],
 | 
			
		||||
  radios: Response['radios']['results'],
 | 
			
		||||
  podcasts: Response['podcasts']['results'],
 | 
			
		||||
  series: Response['series']['results'],
 | 
			
		||||
  rss: [Response['rss']],
 | 
			
		||||
  federation: [Response['federation']]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const responses = ref<Partial<Response>>({})
 | 
			
		||||
const results = ref<Partial<Results>>({})
 | 
			
		||||
 | 
			
		||||
const categories = computed(() => [
 | 
			
		||||
  {
 | 
			
		||||
    type: 'artists',
 | 
			
		||||
    label: t('views.Search.label.artists'),
 | 
			
		||||
    more: '/library/artists',
 | 
			
		||||
    endpoint: '/search',
 | 
			
		||||
    params: {
 | 
			
		||||
      contentCategory: 'music',
 | 
			
		||||
      includeChannels: 'true',
 | 
			
		||||
      page: 1,
 | 
			
		||||
      page_size: 4
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'albums',
 | 
			
		||||
    label: t('views.Search.label.albums'),
 | 
			
		||||
    more: '/library/albums',
 | 
			
		||||
    endpoint: '/search',
 | 
			
		||||
    params: {
 | 
			
		||||
      contentCategory: 'music',
 | 
			
		||||
      includeChannels: 'true',
 | 
			
		||||
      page: 1,
 | 
			
		||||
      page_size: 4
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'tracks',
 | 
			
		||||
    label: t('views.Search.label.tracks'),
 | 
			
		||||
    endpoint: '/search',
 | 
			
		||||
    params: {
 | 
			
		||||
      page: 1,
 | 
			
		||||
      page_size: 24
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'tags',
 | 
			
		||||
    label: t('views.Search.label.tags'),
 | 
			
		||||
    endpoint: '/search',
 | 
			
		||||
    params: {
 | 
			
		||||
      page: 1,
 | 
			
		||||
      page_size: 24
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'playlists',
 | 
			
		||||
    label: t('views.Search.label.playlists'),
 | 
			
		||||
    more: '/library/playlists/',
 | 
			
		||||
    endpoint: '/TODO'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'radios',
 | 
			
		||||
    label: t('views.Search.label.radios'),
 | 
			
		||||
    more: '/library/radios',
 | 
			
		||||
    endpoint: '/radios/radios/',
 | 
			
		||||
    params: {
 | 
			
		||||
      page: 1,
 | 
			
		||||
      page_size: 4
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'podcasts',
 | 
			
		||||
    label: t('views.Search.label.podcasts'),
 | 
			
		||||
    more: '/library/podcasts',
 | 
			
		||||
    endpoint: '/artists/',
 | 
			
		||||
    params: {
 | 
			
		||||
      contentCategory: 'podcast',
 | 
			
		||||
      includeChannels: 'true',
 | 
			
		||||
      page: 1,
 | 
			
		||||
      page_size: 4
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'series',
 | 
			
		||||
    label: t('views.Search.label.series'),
 | 
			
		||||
    endpoint: '/albums/',
 | 
			
		||||
    params: {
 | 
			
		||||
      contentCategory: 'podcast',
 | 
			
		||||
      includeChannels: 'true',
 | 
			
		||||
      page: 1,
 | 
			
		||||
      page_size: 4
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'rss',
 | 
			
		||||
    label: t('views.Search.header.rss'),
 | 
			
		||||
    endpoint: '/channels/rss-subscribe/',
 | 
			
		||||
    post: true,
 | 
			
		||||
    params: {
 | 
			
		||||
      url: trimmedQuery.value
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    type: 'federation',
 | 
			
		||||
    label: t('views.Search.header.remote'),
 | 
			
		||||
    endpoint: '/federation/fetches/',
 | 
			
		||||
    post: true,
 | 
			
		||||
    params: {
 | 
			
		||||
      object: trimmedQuery.value
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
] as const satisfies {
 | 
			
		||||
  type: Category
 | 
			
		||||
  label: string
 | 
			
		||||
  post?: true
 | 
			
		||||
  more?: string
 | 
			
		||||
  params?: {
 | 
			
		||||
    [key: string]: string | number
 | 
			
		||||
  }
 | 
			
		||||
  endpoint: `/${string}`
 | 
			
		||||
}[])
 | 
			
		||||
 | 
			
		||||
// Limit the available categories based on the search query
 | 
			
		||||
// Show fetch if the query is a URL; show RSS if the query is an email address; show all other cateories otherwise
 | 
			
		||||
const availableCategories = computed(() =>
 | 
			
		||||
  categories.value.filter(({ type }) =>
 | 
			
		||||
    isFetch.value ? type === 'federation'
 | 
			
		||||
      : isRss.value ? type === 'rss'
 | 
			
		||||
        : type !== 'federation' && type !== 'rss'
 | 
			
		||||
))
 | 
			
		||||
 | 
			
		||||
// Whenever available categories change, if there is exactly one, open it
 | 
			
		||||
watch(availableCategories, () => {
 | 
			
		||||
  if (availableCategories.value.length === 1)
 | 
			
		||||
    openSections.value = new Set(availableCategories.value.map(category => category.type))
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get a list of the loaded results for a given category (max. 4)
 | 
			
		||||
 * @param category The category to get the results for
 | 
			
		||||
 * @returns The results for the given category, in the form of an Array; `[]` if the category has not yet been queried
 | 
			
		||||
 */
 | 
			
		||||
const resultsPerCategory = <C extends Category>(category: { type: C }) =>
 | 
			
		||||
  results.value[category.type] || []
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get the total number of results
 | 
			
		||||
 * @param category The category to get the results for
 | 
			
		||||
 * @returns The number of results for the given category according to the backend; `0` if the category has not yet been queried
 | 
			
		||||
 */
 | 
			
		||||
const count = <C extends Category>(category: { type: C }) => (
 | 
			
		||||
  response => response && 'count' in response ? response.count : resultsPerCategory(category).length
 | 
			
		||||
) (responses.value[category.type])
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Find out whether a category has been queried before
 | 
			
		||||
 * @param category The category to which may have been queried
 | 
			
		||||
 */
 | 
			
		||||
const isCategoryQueried = <C extends Category>(category: { type: C }) =>
 | 
			
		||||
  results.value[category.type] ? true : false
 | 
			
		||||
 | 
			
		||||
// Display
 | 
			
		||||
 | 
			
		||||
const openCategories = computed(() =>
 | 
			
		||||
  categories.value.filter(({ type }) => openSections.value.has(type))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Sections can be manually or automatically toggled
 | 
			
		||||
 | 
			
		||||
const openSections = ref<Set<Category>>(new Set())
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * If no results are in currently expanded categories but some collapsed have results, show those
 | 
			
		||||
*/
 | 
			
		||||
watch(results, () => {
 | 
			
		||||
  if (openCategories.value.some(category => count(category) > 0)) return
 | 
			
		||||
 | 
			
		||||
  const categoriesWithResults
 | 
			
		||||
    = availableCategories.value.filter(category => count(category) > 0)
 | 
			
		||||
 | 
			
		||||
  if (categoriesWithResults.length === 0) return
 | 
			
		||||
 | 
			
		||||
  openSections.value = new Set(categoriesWithResults.map(({ type }) => type))
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// Search
 | 
			
		||||
 | 
			
		||||
const search = async () => {
 | 
			
		||||
 | 
			
		||||
  // Close if query is empty
 | 
			
		||||
  if (trimmedQuery.value.length < 1) {
 | 
			
		||||
    isOpen.value = false
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const params = new URLSearchParams({
 | 
			
		||||
    q: queryDebounced.value,
 | 
			
		||||
    ...(openCategories.value && 'params' in openCategories.value && openCategories.value.params
 | 
			
		||||
      ? openCategories.value.params
 | 
			
		||||
      : {}
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Only query category that are open / available. Omit duplicate queries (`uniqBy`).
 | 
			
		||||
  const categories
 | 
			
		||||
    = uniqBy(
 | 
			
		||||
      openCategories.value.length > 0
 | 
			
		||||
        ? openCategories.value
 | 
			
		||||
        : availableCategories.value,
 | 
			
		||||
      (category => category.endpoint + ('params' in category && JSON.stringify(category.params)))
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  isLoading.value = true
 | 
			
		||||
 | 
			
		||||
  for (const category of categories) {
 | 
			
		||||
    try {
 | 
			
		||||
      if (category.endpoint === '/search') {
 | 
			
		||||
        const response = await axios.get<Response[typeof category.type]>(
 | 
			
		||||
          category.endpoint,
 | 
			
		||||
          { params }
 | 
			
		||||
        )
 | 
			
		||||
        // Store the four search results
 | 
			
		||||
        results.value = {
 | 
			
		||||
          ...results.value,
 | 
			
		||||
          ...response.data
 | 
			
		||||
        }
 | 
			
		||||
        responses.value[category.type] = response.data
 | 
			
		||||
      } else {
 | 
			
		||||
        if (category.type === 'rss') {
 | 
			
		||||
          const response = await axios.post<Response['rss']>(
 | 
			
		||||
            category.endpoint,
 | 
			
		||||
            { url: trimmedQuery.value }
 | 
			
		||||
          )
 | 
			
		||||
          results.value.rss = [response.data]
 | 
			
		||||
          responses.value[category.type] = response.data
 | 
			
		||||
        } else if (category.type === 'federation') {
 | 
			
		||||
          const response = await axios.post<Response['federation']>(
 | 
			
		||||
            category.endpoint,
 | 
			
		||||
            { params }
 | 
			
		||||
          )
 | 
			
		||||
          results.value.federation = [response.data]
 | 
			
		||||
          responses.value[category.type] = response.data
 | 
			
		||||
        } else if (category.type === 'playlists') {
 | 
			
		||||
          const response = await axios.get<Response['playlists']>(
 | 
			
		||||
            category.endpoint,
 | 
			
		||||
            { params }
 | 
			
		||||
          )
 | 
			
		||||
          results.value.playlists = response.data.results
 | 
			
		||||
          responses.value[category.type] = response.data
 | 
			
		||||
        } else if (category.type === 'podcasts') {
 | 
			
		||||
          const response = await axios.get<Response['podcasts']>(
 | 
			
		||||
            category.endpoint,
 | 
			
		||||
            { params }
 | 
			
		||||
          )
 | 
			
		||||
          results.value.podcasts = response.data.results
 | 
			
		||||
          responses.value[category.type] = response.data
 | 
			
		||||
        } else if (category.type === 'radios') {
 | 
			
		||||
          const response = await axios.get<Response['radios']>(
 | 
			
		||||
            category.endpoint,
 | 
			
		||||
            { params }
 | 
			
		||||
          )
 | 
			
		||||
          results.value.radios = response.data.results
 | 
			
		||||
          responses.value[category.type] = response.data
 | 
			
		||||
        } else if (category.type === 'series') {
 | 
			
		||||
          const response = await axios.get<Response['series']>(
 | 
			
		||||
            category.endpoint,
 | 
			
		||||
            { params }
 | 
			
		||||
          )
 | 
			
		||||
          results.value.series = response.data.results
 | 
			
		||||
          responses.value[category.type] = response.data
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      useErrorHandler(error as Error)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
  isLoading.value = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Configure the radio
 | 
			
		||||
 | 
			
		||||
const radioConfig = computed<RadioConfig | null>(() =>
 | 
			
		||||
  count({ type: 'tags' }) > 0
 | 
			
		||||
    ? ({
 | 
			
		||||
        type: 'tag',
 | 
			
		||||
        names: resultsPerCategory({ type: 'tags' })
 | 
			
		||||
          .map((({ name }) => name))
 | 
			
		||||
      })
 | 
			
		||||
    : count({ type: 'playlists' }) > 0
 | 
			
		||||
      ? ({
 | 
			
		||||
          type: 'playlist',
 | 
			
		||||
          ids: resultsPerCategory({ type: 'playlists' }).map(({ id }) => id.toString())
 | 
			
		||||
        })
 | 
			
		||||
      : count({ type: 'artists' }) > 0
 | 
			
		||||
        ? ({
 | 
			
		||||
            type: 'artist',
 | 
			
		||||
            ids: resultsPerCategory({ type: 'artists' }).map(({ id }) => id.toString())
 | 
			
		||||
          })
 | 
			
		||||
        : null
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Start the search
 | 
			
		||||
 | 
			
		||||
watch(queryDebounced, search, { immediate: true })
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Modal
 | 
			
		||||
    v-model="isOpen"
 | 
			
		||||
    over-popover
 | 
			
		||||
    autofocus="off"
 | 
			
		||||
    title=""
 | 
			
		||||
  >
 | 
			
		||||
    <template #topleft>
 | 
			
		||||
      <Input
 | 
			
		||||
        v-model="query"
 | 
			
		||||
        raised
 | 
			
		||||
        :autofocus="openCategories.length===0"
 | 
			
		||||
        icon="bi-search"
 | 
			
		||||
      />
 | 
			
		||||
      <RadioButton
 | 
			
		||||
        v-if="radioConfig"
 | 
			
		||||
        class="ui right floated medium button"
 | 
			
		||||
        type="custom_multiple"
 | 
			
		||||
        :radio-config="radioConfig"
 | 
			
		||||
      />
 | 
			
		||||
    </template>
 | 
			
		||||
    <Spacer />
 | 
			
		||||
 | 
			
		||||
    <Loader
 | 
			
		||||
      v-if="isLoading"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <template
 | 
			
		||||
      v-for="category in availableCategories"
 | 
			
		||||
      :key="category.type + isCategoryQueried(category)"
 | 
			
		||||
    >
 | 
			
		||||
      <Section
 | 
			
		||||
        align-left
 | 
			
		||||
        :columns-per-item="1"
 | 
			
		||||
        :h3="`${
 | 
			
		||||
          !isCategoryQueried(category)
 | 
			
		||||
            ? '...'
 | 
			
		||||
            : count(category) > 0
 | 
			
		||||
              ? `${count(category)} `
 | 
			
		||||
              : ''
 | 
			
		||||
        }${category.label}`"
 | 
			
		||||
        v-bind="
 | 
			
		||||
          openSections.has(category.type)
 | 
			
		||||
            ? ({ collapse: () => openSections.delete(category.type) })
 | 
			
		||||
            : ({ expand: () => openSections.add(category.type) })
 | 
			
		||||
        "
 | 
			
		||||
      >
 | 
			
		||||
        <!-- Categories that have one list-style item -->
 | 
			
		||||
 | 
			
		||||
        <TrackTable
 | 
			
		||||
          v-if="category.type === 'tracks'"
 | 
			
		||||
          style="grid-column: 1 / -1"
 | 
			
		||||
          :tracks="resultsPerCategory(category)"
 | 
			
		||||
        />
 | 
			
		||||
        <TagsList
 | 
			
		||||
          v-else-if="category.type === 'tags'"
 | 
			
		||||
          style="grid-column: 1 / -1"
 | 
			
		||||
          :truncate-size="200"
 | 
			
		||||
          :limit="category.params.page_size"
 | 
			
		||||
          :tags="(resultsPerCategory(category)).map(t => t.name)"
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <!-- Categories that show individual cards -->
 | 
			
		||||
        <template
 | 
			
		||||
          v-for="(_, index) in (resultsPerCategory(category))"
 | 
			
		||||
          :key="category.type + index"
 | 
			
		||||
        >
 | 
			
		||||
          <ArtistCard
 | 
			
		||||
            v-if="(category.type === 'artists' || category.type === 'podcasts')"
 | 
			
		||||
            :artist="resultsPerCategory(category)[index]"
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <AlbumCard
 | 
			
		||||
            v-else-if="category.type === 'albums' || category.type === 'series'"
 | 
			
		||||
            :album="resultsPerCategory(category)[index]"
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <PlaylistCard
 | 
			
		||||
            v-else-if="category.type === 'playlists'"
 | 
			
		||||
            :playlist="resultsPerCategory(category)[index]"
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <RadioCard
 | 
			
		||||
            v-else-if="category.type === 'radios'"
 | 
			
		||||
            type="custom"
 | 
			
		||||
            :custom-radio="resultsPerCategory(category)[index]"
 | 
			
		||||
          />
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <!-- If response has "url": "webfinger://node1@node1.funkwhale.test" -> Link to go directly to the federation page -->
 | 
			
		||||
 | 
			
		||||
        <span v-if="category.type === 'rss' && count(category) > 0">
 | 
			
		||||
          <Alert>{{ t('modals.search.tryAgain') }}</Alert>
 | 
			
		||||
          <Link
 | 
			
		||||
            v-for="channel in resultsPerCategory(category)"
 | 
			
		||||
            :key="channel.artist.fid"
 | 
			
		||||
            :to="channel.artist.fid"
 | 
			
		||||
            autofocus
 | 
			
		||||
          >
 | 
			
		||||
            {{ channel.artist.name }}
 | 
			
		||||
          </Link>
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <span v-else-if="category.type === 'federation'">
 | 
			
		||||
          <!-- TODO: Federation search: backend adapter + display, fix results_per_category query -->
 | 
			
		||||
          <!-- {{ resultsPerCategory(category) }} -->
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <EmptyState
 | 
			
		||||
          v-if="count(category) === 0"
 | 
			
		||||
          style="grid-column: 1 / -1"
 | 
			
		||||
          :refresh="true"
 | 
			
		||||
          @refresh="search"
 | 
			
		||||
        />
 | 
			
		||||
        <Link
 | 
			
		||||
          v-else-if="'more' in category"
 | 
			
		||||
          solid
 | 
			
		||||
          secondary
 | 
			
		||||
          :to="category.more"
 | 
			
		||||
        >
 | 
			
		||||
          {{ t('components.Home.link.viewMore') }}
 | 
			
		||||
        </Link>
 | 
			
		||||
      </Section>
 | 
			
		||||
    </template>
 | 
			
		||||
  </Modal>
 | 
			
		||||
</template>
 | 
			
		||||
		Loading…
	
		Reference in New Issue