sepia-search-motore-di-rice.../client/src/views/Search.vue

583 lines
17 KiB
Vue

<template>
<div>
<my-header :index-name="indexName" :small-format="true" />
<main>
<form id="search-anchor" role="search" onsubmit="return false;">
<div class="card position-relative">
<button class="warning-button popover-button">
<div class="visually-hidden">{{ $gettext('Toggle warning information') }}</div>
<icon-warning></icon-warning>
</button>
<search-warning class="d-none popover-content" :index-name="indexName" :is-block-mode="false" />
<search-input class="search-input" :label="true"></search-input>
</div>
<div class="button-rows">
<button
class="filters-button peertube-button peertube-primary-button"
:aria-expanded="displayFilters" aria-controls="filters" @click="toggleFilters()"
>
{{ $gettext('Filters') }}
<template v-if="activeFilters">({{ activeFilters }})</template>
<div class="arrow-down" />
</button>
<sort-button></sort-button>
</div>
<filters v-if="displayFilters" id="filters"></filters>
</form>
<div v-if="searched" class="results">
<div class="results-summary">
<span v-if="totalResults === 0">{{ $gettext('No results found.') }}</span>
<span v-if="totalResults">{{ $ngettext('%{totalResults} result found:', '%{totalResults} results found:', totalResults, { totalResults: totalResults.toLocaleString() }) }}</span>
</div>
<div v-if="(!hasResultTypeFilter() || getResultTypeFilter() === 'videos') && totalResults !== 0">
<h2>
<strong>
{{ $ngettext('%{totalVideos} video', '%{totalVideos} videos', totalVideos, { totalVideos: totalVideos + '' }) }}
</strong>
</h2>
<template v-if="totalVideos">
<div v-for="video in videos" :key="video.url" class="mb-4">
<video-result :video="video"></video-result>
</div>
<div v-if="!hasResultTypeFilter()" class="mb-5 text-center">
<router-link
v-if="!hasResultTypeFilter() && totalVideos > summaryResultsCount.videos"
class="peertube-button peertube-button-link peertube-secondary-button"
:to="{ path: '/search', query: { ...getMoreResultsQuery('videos') } }"
>
{{ $gettext('Display more videos') }}
</router-link>
</div>
</template>
</div>
<div v-if="(!hasResultTypeFilter() || getResultTypeFilter() === 'channels') && totalResults !== 0">
<h2>
<strong>
{{ $ngettext('%{totalChannels} channel', '%{totalChannels} channels', totalChannels, { totalChannels: totalChannels + '' }) }}
</strong>
</h2>
<template v-if="totalChannels">
<div v-for="channel in channels" :key="channel.url" class="mb-4">
<channel-result :channel="channel"></channel-result>
</div>
<div class="mb-5 text-center">
<router-link
v-if="!hasResultTypeFilter() && totalChannels > summaryResultsCount.channels"
class="peertube-button peertube-button-link peertube-secondary-button"
:to="{ path: '/search', query: { ...getMoreResultsQuery('channels') } }"
>
{{ $gettext('Display more channels') }}
</router-link>
</div>
</template>
</div>
<div v-if="(!hasResultTypeFilter() || getResultTypeFilter() === 'playlists') && totalResults !== 0">
<h2>
<strong>
{{ $ngettext('%{totalPlaylists} playlist', '%{totalPlaylists} playlists', totalPlaylists, { totalPlaylists: totalPlaylists + '' }) }}
</strong>
</h2>
<template v-if="totalPlaylists">
<div v-for="playlist in playlists" :key="playlist.url" class="mb-4">
<playlist-result :playlist="playlist"></playlist-result>
</div>
<div class="mb-5 text-center">
<router-link
v-if="!hasResultTypeFilter() && totalPlaylists > summaryResultsCount.playlists"
class="peertube-button peertube-button-link peertube-secondary-button"
:to="{ path: '/search', query: { ...getMoreResultsQuery('playlists') } }"
>
{{ $gettext('Display more playlists') }}
</router-link>
</div>
</template>
</div>
</div>
<pagination v-if="hasResultTypeFilter()" v-model="currentPage" :max-page="getMaxPage()" :searched="searched" :pages="pages" />
</main>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Header from '../components/Header.vue'
import SearchWarning from '../components/SearchWarning.vue'
import VideoResult from '../components/VideoResult.vue'
import ChannelResult from '../components/ChannelResult.vue'
import PlaylistResult from '../components/PlaylistResult.vue'
import IconWarning from '../components/icons/IconWarning.vue'
import Filters from '../components/Filters.vue'
import { searchVideos, searchVideoChannels, searchVideoPlaylists } from '../shared/search'
import { getConfig } from '../shared/config'
import { pageToAPIParams, durationRangeToAPIParams, publishedDateRangeToAPIParams, extractTagsFromQuery, extractQueryToIntArray, extractQueryToStringArray, extractQueryToInt, extractQueryToBoolean } from '../shared/utils'
import { SearchUrl } from '../models'
import { APIVideo } from '../../../server/types/video.model'
import { APIVideoChannel } from '../../../server/types/channel.model'
import Pagination from '../components/Pagination.vue'
import SearchInput from '../components/SearchInput.vue'
import SortButton from '../components/SortButton.vue'
import type { VideoChannelsSearchQuery, ResultList, VideosSearchQuery } from '@peertube/peertube-types'
import Nprogress from 'nprogress'
import { APIPlaylist } from '../../../server/types/playlist.model'
import { PlaylistsSearchQuery } from '../../../server/types/search-query/playlist-search.model'
export default defineComponent({
components: {
'my-header': Header,
'search-warning': SearchWarning,
'video-result': VideoResult,
'channel-result': ChannelResult,
'playlist-result': PlaylistResult,
'search-input': SearchInput,
'pagination': Pagination,
'filters': Filters,
'sort-button': SortButton,
'icon-warning': IconWarning
},
data () {
return {
searched: false,
indexName: '',
totalResults: null as number,
totalVideos: null as number,
videos: null as APIVideo[],
totalChannels: null as number,
channels: null as APIVideoChannel[],
totalPlaylists: null as number,
playlists: null as APIPlaylist[],
currentPage: 1,
pages: [],
summaryResultsCount: {
videos: 5,
channels: 2,
playlists: 2
},
filteredTypeResultsPerPage: 10,
activeFilters: 0,
displayFilters: false
}
},
computed: {
boostLanguagesQuery (): string[] {
const languages = new Set<string>()
for (const completeLanguage of navigator.languages) {
languages.add(completeLanguage.split('-')[0])
}
return Array.from(languages)
}
},
watch: {
$route() {
this.loadUrl()
this.doSearch()
}
},
mounted () {
// eslint-disable-next-line no-new
import('bootstrap/js/dist/popover')
.then(Popover => {
new Popover.default(this.$el.querySelector('.popover-button'), {
content: document.querySelector('.popover-content'),
sanitize: false,
trigger: 'focus'
})
})
const config = getConfig()
this.indexName = config.searchInstanceName
if (Object.keys(this.$route.query).length !== 0) {
this.loadUrl()
this.doSearch()
}
},
methods: {
async doSearch () {
this.videos = []
this.channels = []
this.playlists = []
this.searched = false
Nprogress.start()
try {
const [ videosResult, channelsResult, playlistsResult ] = await Promise.all([
this.searchVideos(),
this.searchChannels(),
this.searchPlaylists()
])
Nprogress.done()
this.activeFilters = this.countActiveFilters()
this.totalVideos = videosResult.total
this.videos = videosResult.data
this.totalChannels = channelsResult.total
this.channels = channelsResult.data
this.totalPlaylists = playlistsResult.total
this.playlists = playlistsResult.data
this.totalResults = videosResult.total + channelsResult.total + playlistsResult.total
this.buildPages()
this.searched = true
} catch (err) {
console.error('Cannot do search.', err)
} finally {
this.searched = true
Nprogress.done()
}
},
loadUrl () {
const query = this.$route.query as SearchUrl
const queryPage = extractQueryToInt(query.page)
this.currentPage = queryPage && this.currentPage !== queryPage
? queryPage
: 1
},
countActiveFilters () {
const query = this.$route.query as SearchUrl
let count = 0
if (query.nsfw) count++
if (query.host) count++
if (query.publishedDateRange) count++
if (query.durationRange) count++
if (query.categoryOneOf) count++
if (query.licenceOneOf) count++
if (query.languageOneOf) count++
if (query.isLive) count++
if (query.resultType) count++
if (Array.isArray(query.tagsAllOf) && query.tagsAllOf.length !== 0) count++
if (Array.isArray(query.tagsOneOf) && query.tagsOneOf.length !== 0) count++
return count
},
buildVideoSearchQuery () {
const query = this.$route.query as SearchUrl
const resultsPerPage = this.getResultTypeFilter() === 'videos'
? this.filteredTypeResultsPerPage
: this.summaryResultsCount.videos
const { start, count } = pageToAPIParams(this.currentPage, resultsPerPage)
const { durationMin, durationMax } = durationRangeToAPIParams(query.durationRange)
const { startDate, endDate } = publishedDateRangeToAPIParams(query.publishedDateRange)
const boostLanguages = this.boostLanguagesQuery
let sort: string
if (query.sort === '-matched') sort = '-matched'
else if (query.sort === '-createdAt') sort = '-publishedAt'
else if (query.sort === 'createdAt') sort = 'publishedAt'
return {
search: query.search,
durationMin,
durationMax,
startDate,
endDate,
boostLanguages,
nsfw: query.nsfw || false,
categoryOneOf: extractQueryToIntArray(query.categoryOneOf),
licenceOneOf: extractQueryToIntArray(query.licenceOneOf),
languageOneOf: extractQueryToStringArray(query.languageOneOf),
tagsOneOf: extractTagsFromQuery(query.tagsOneOf).map(t => t.text),
tagsAllOf: extractTagsFromQuery(query.tagsAllOf).map(t => t.text),
isLive: extractQueryToBoolean(query.isLive),
host: query.host || undefined,
start,
count,
sort
} as VideosSearchQuery
},
buildChannelSearchQuery () {
const query = this.$route.query as SearchUrl
const resultsPerPage = this.getResultTypeFilter() === 'channels'
? this.filteredTypeResultsPerPage
: this.summaryResultsCount.channels
const { start, count } = pageToAPIParams(this.currentPage, resultsPerPage)
return {
search: query.search,
host: query.host || undefined,
start,
sort: query.sort,
count
} as VideoChannelsSearchQuery
},
buildPlaylistSearchQuery () {
const query = this.$route.query as SearchUrl
const resultsPerPage = this.getResultTypeFilter() === 'playlists'
? this.filteredTypeResultsPerPage
: this.summaryResultsCount.playlists
const { start, count } = pageToAPIParams(this.currentPage, resultsPerPage)
return {
search: query.search,
host: query.host || undefined,
start,
sort: query.sort,
count
} as PlaylistsSearchQuery
},
searchVideos (): Promise<ResultList<APIVideo>> {
if (this.isVideoSearchDisabled()) {
return Promise.resolve({ total: 0, data: [] })
}
const query = this.buildVideoSearchQuery()
return searchVideos(query)
},
searchChannels (): Promise<ResultList<APIVideoChannel>> {
if (this.isChannelSearchDisabled()) {
return Promise.resolve({ data: [], total: 0 })
}
const query = this.buildChannelSearchQuery()
return searchVideoChannels(query)
},
searchPlaylists (): Promise<ResultList<APIPlaylist>> {
if (this.isPlaylistSearchDisabled()) {
return Promise.resolve({ data: [], total: 0 })
}
const query = this.buildPlaylistSearchQuery()
return searchVideoPlaylists(query)
},
// ---------------------------------------------------------------------------
getMaxPage () {
const resultType = (this.$route.query as SearchUrl).resultType
let maxPage: number
if (resultType === 'videos') maxPage = Math.ceil(this.totalVideos / this.filteredTypeResultsPerPage)
else if (resultType === 'channels') maxPage = Math.ceil(this.totalChannels / this.filteredTypeResultsPerPage)
else if (resultType === 'playlists') maxPage = Math.ceil(this.totalPlaylists / this.filteredTypeResultsPerPage)
// Limit to 10 pages
return Math.min(10, maxPage)
},
buildPages () {
this.pages = []
for (let i = 1; i <= this.getMaxPage(); i++) {
this.pages.push(i)
}
},
toggleFilters () {
this.displayFilters = !this.displayFilters
},
// ---------------------------------------------------------------------------
getResultTypeFilter () {
return (this.$route.query as SearchUrl).resultType
},
hasResultTypeFilter () {
return !!this.getResultTypeFilter()
},
getMoreResultsQuery (type: 'videos' | 'channels' | 'playlists'): SearchUrl {
return {
...this.$route.query,
resultType: type
}
},
// ---------------------------------------------------------------------------
isVideoSearchDisabled () {
const { resultType } = this.$route.query as SearchUrl
if (resultType !== undefined && resultType !== 'videos') return true
return false
},
isChannelSearchDisabled () {
const { resultType, host } = this.$route.query as SearchUrl
if (resultType !== undefined) {
if (resultType !== 'channels') return true
return false
}
// We can search against host for playlists and channels
if (host) return this.countActiveFilters() > 1
return this.countActiveFilters() > 0
},
isPlaylistSearchDisabled () {
const { resultType, host } = this.$route.query as SearchUrl
if (resultType !== undefined) {
if (resultType !== 'playlists') return true
return false
}
// We can search against host for playlists and channels
if (host) return this.countActiveFilters() > 1
return this.countActiveFilters() > 0
}
}
})
</script>
<style scoped lang="scss">
@use 'sass:math';
@import '../scss/_variables';
@import '../scss/bootstrap-mixins';
.search-input {
max-width: 700px;
margin: auto;
}
.warning-button {
background: transparent;
border: none;
position: absolute;
top: 10px;
right: 10px;
svg {
width: 30px;
height: 30px;
}
&:hover {
opacity: 0.8;
}
}
.filters-button {
position: relative;
padding-left: 1rem;
padding-right: 2rem;
.arrow-down {
$size: 4px;
position: absolute;
right: 0;
top: 50%;
margin-top: math.div(-$size, 2);
margin-left: 1.5rem;
margin-right: 0.75rem;
width: 0;
height: 0;
border-left: $size solid transparent;
border-right: $size solid transparent;
border-top: $size solid #fff;
}
}
.button-rows {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
> * {
@include margin-top(2rem);
}
}
.results {
@include margin(2rem 0);
}
.results-summary {
@include margin-bottom(3rem);
font-weight: $font-semibold;
text-align: center;
}
h2 {
@include font-size(1.125rem);
}
</style>