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

583 lines
17 KiB
Vue
Raw Normal View History

2020-08-27 14:44:21 +02:00
<template>
<div>
2022-12-16 14:44:54 +01:00
<my-header :index-name="indexName" :small-format="true" />
2020-08-27 14:44:21 +02:00
<main>
2022-12-16 14:44:54 +01:00
<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>
2020-08-27 14:44:21 +02:00
2022-12-16 14:44:54 +01:00
<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()"
2022-02-23 15:21:02 +01:00
>
2022-12-16 14:44:54 +01:00
{{ $gettext('Filters') }}
<template v-if="activeFilters">({{ activeFilters }})</template>
<div class="arrow-down" />
</button>
<sort-button></sort-button>
2020-08-27 14:44:21 +02:00
</div>
2022-12-16 14:44:54 +01:00
<filters v-if="displayFilters" id="filters"></filters>
</form>
2022-12-28 13:41:10 +01:00
<div v-if="searched" class="results">
2022-12-16 14:44:54 +01:00
<div class="results-summary">
2022-12-28 13:41:10 +01:00
<span v-if="totalResults === 0">{{ $gettext('No results found.') }}</span>
2020-09-16 10:54:52 +02:00
2022-12-28 13:41:10 +01:00
<span v-if="totalResults">{{ $ngettext('%{totalResults} result found:', '%{totalResults} results found:', totalResults, { totalResults: totalResults.toLocaleString() }) }}</span>
2020-08-27 14:44:21 +02:00
</div>
2022-12-28 13:41:10 +01:00
<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">
2022-12-28 13:41:10 +01:00
<router-link
v-if="!hasResultTypeFilter() && totalVideos > summaryResultsCount.videos"
2022-12-28 13:41:10 +01:00
class="peertube-button peertube-button-link peertube-secondary-button"
:to="{ path: '/search', query: { ...getMoreResultsQuery('videos') } }"
>
{{ $gettext('Display more videos') }}
</router-link>
</div>
</template>
</div>
2022-12-16 14:44:54 +01:00
2022-12-28 13:41:10 +01:00
<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"
2022-12-28 13:41:10 +01:00
class="peertube-button peertube-button-link peertube-secondary-button"
:to="{ path: '/search', query: { ...getMoreResultsQuery('channels') } }"
>
{{ $gettext('Display more channels') }}
</router-link>
</div>
</template>
2020-08-27 14:44:21 +02:00
</div>
2022-12-28 13:41:10 +01:00
<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"
2022-12-28 13:41:10 +01:00
class="peertube-button peertube-button-link peertube-secondary-button"
:to="{ path: '/search', query: { ...getMoreResultsQuery('playlists') } }"
>
{{ $gettext('Display more playlists') }}
</router-link>
</div>
</template>
</div>
2020-08-27 14:44:21 +02:00
</div>
2022-12-28 13:41:10 +01:00
<pagination v-if="hasResultTypeFilter()" v-model="currentPage" :max-page="getMaxPage()" :searched="searched" :pages="pages" />
2020-08-27 14:44:21 +02:00
</main>
</div>
</template>
<script lang="ts">
2021-06-25 11:44:32 +02:00
import { defineComponent } from 'vue'
2020-08-27 14:44:21 +02:00
import Header from '../components/Header.vue'
import SearchWarning from '../components/SearchWarning.vue'
import VideoResult from '../components/VideoResult.vue'
import ChannelResult from '../components/ChannelResult.vue'
2021-06-24 16:53:43 +02:00
import PlaylistResult from '../components/PlaylistResult.vue'
2022-12-16 14:44:54 +01:00
import IconWarning from '../components/icons/IconWarning.vue'
import Filters from '../components/Filters.vue'
2021-06-24 16:53:43 +02:00
import { searchVideos, searchVideoChannels, searchVideoPlaylists } from '../shared/search'
2020-08-27 14:44:21 +02:00
import { getConfig } from '../shared/config'
2022-12-16 14:44:54 +01:00
import { pageToAPIParams, durationRangeToAPIParams, publishedDateRangeToAPIParams, extractTagsFromQuery, extractQueryToIntArray, extractQueryToStringArray, extractQueryToInt, extractQueryToBoolean } from '../shared/utils'
2020-08-27 14:44:21 +02:00
import { SearchUrl } from '../models'
2023-11-10 14:14:57 +01:00
import { APIVideo } from '../../../server/types/video.model'
import { APIVideoChannel } from '../../../server/types/channel.model'
2020-08-27 14:44:21 +02:00
import Pagination from '../components/Pagination.vue'
2022-12-16 14:44:54 +01:00
import SearchInput from '../components/SearchInput.vue'
import SortButton from '../components/SortButton.vue'
2023-11-13 11:01:17 +01:00
import type { VideoChannelsSearchQuery, ResultList, VideosSearchQuery } from '@peertube/peertube-types'
2020-09-02 15:33:15 +02:00
import Nprogress from 'nprogress'
2023-11-10 14:14:57 +01:00
import { APIPlaylist } from '../../../server/types/playlist.model'
2021-06-24 16:53:43 +02:00
import { PlaylistsSearchQuery } from '../../../server/types/search-query/playlist-search.model'
2020-08-27 14:44:21 +02:00
2021-06-25 11:44:32 +02:00
export default defineComponent({
2020-08-27 14:44:21 +02:00
components: {
'my-header': Header,
'search-warning': SearchWarning,
'video-result': VideoResult,
'channel-result': ChannelResult,
2021-06-24 16:53:43 +02:00
'playlist-result': PlaylistResult,
2022-12-16 14:44:54 +01:00
'search-input': SearchInput,
'pagination': Pagination,
'filters': Filters,
'sort-button': SortButton,
'icon-warning': IconWarning
2020-08-27 14:44:21 +02:00
},
data () {
return {
2022-12-16 14:44:54 +01:00
searched: false,
2020-08-27 14:44:21 +02:00
indexName: '',
2022-12-28 13:41:10 +01:00
totalResults: null as number,
totalVideos: null as number,
2023-11-10 14:14:57 +01:00
videos: null as APIVideo[],
2022-12-28 13:41:10 +01:00
totalChannels: null as number,
2023-11-10 14:14:57 +01:00
channels: null as APIVideoChannel[],
2022-12-28 13:41:10 +01:00
totalPlaylists: null as number,
2023-11-10 14:14:57 +01:00
playlists: null as APIPlaylist[],
2020-08-27 14:44:21 +02:00
currentPage: 1,
pages: [],
2022-12-28 13:41:10 +01:00
summaryResultsCount: {
videos: 5,
channels: 2,
playlists: 2
},
filteredTypeResultsPerPage: 10,
2020-08-27 14:44:21 +02:00
2022-12-16 14:44:54 +01:00
activeFilters: 0,
2022-12-28 13:41:10 +01:00
displayFilters: false
2020-08-27 14:44:21 +02:00
}
},
computed: {
2021-06-25 11:44:32 +02:00
boostLanguagesQuery (): string[] {
2020-09-23 11:17:43 +02:00
const languages = new Set<string>()
for (const completeLanguage of navigator.languages) {
2022-12-16 14:44:54 +01:00
languages.add(completeLanguage.split('-')[0])
2020-09-23 11:17:43 +02:00
}
return Array.from(languages)
2020-08-27 14:44:21 +02:00
}
},
2022-02-23 15:21:02 +01:00
watch: {
$route() {
this.loadUrl()
this.doSearch()
}
},
2022-12-28 10:58:51 +01:00
mounted () {
2022-12-16 14:44:54 +01:00
// 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'),
2023-03-01 16:25:06 +01:00
sanitize: false,
trigger: 'focus'
2022-12-16 14:44:54 +01:00
})
})
2022-12-28 10:58:51 +01:00
const config = getConfig()
2022-02-23 15:21:02 +01:00
this.indexName = config.searchInstanceName
if (Object.keys(this.$route.query).length !== 0) {
this.loadUrl()
this.doSearch()
}
},
2020-08-27 14:44:21 +02:00
methods: {
2020-09-16 10:06:09 +02:00
async doSearch () {
2022-12-28 13:41:10 +01:00
this.videos = []
this.channels = []
this.playlists = []
2022-12-16 14:44:54 +01:00
this.searched = false
2020-08-27 14:44:21 +02:00
2020-09-02 15:33:15 +02:00
Nprogress.start()
2020-08-27 14:44:21 +02:00
2020-09-02 15:33:15 +02:00
try {
2021-06-24 16:53:43 +02:00
const [ videosResult, channelsResult, playlistsResult ] = await Promise.all([
2020-09-02 15:33:15 +02:00
this.searchVideos(),
2021-06-24 16:53:43 +02:00
this.searchChannels(),
this.searchPlaylists()
2020-09-02 15:33:15 +02:00
])
2020-08-27 14:44:21 +02:00
2020-09-02 15:33:15 +02:00
Nprogress.done()
2020-08-27 14:44:21 +02:00
2020-09-16 10:30:16 +02:00
this.activeFilters = this.countActiveFilters()
2020-08-27 14:44:21 +02:00
2022-12-28 13:41:10 +01:00
this.totalVideos = videosResult.total
this.videos = videosResult.data
2021-06-25 11:44:32 +02:00
2022-12-28 13:41:10 +01:00
this.totalChannels = channelsResult.total
this.channels = channelsResult.data
this.totalPlaylists = playlistsResult.total
this.playlists = playlistsResult.data
this.totalResults = videosResult.total + channelsResult.total + playlistsResult.total
2020-09-02 15:33:15 +02:00
this.buildPages()
2022-12-16 14:44:54 +01:00
this.searched = true
2020-09-02 15:33:15 +02:00
} catch (err) {
console.error('Cannot do search.', err)
} finally {
2022-12-16 14:44:54 +01:00
this.searched = true
2020-09-02 15:33:15 +02:00
Nprogress.done()
}
2020-08-27 14:44:21 +02:00
},
loadUrl () {
2021-06-25 11:44:32 +02:00
const query = this.$route.query as SearchUrl
2022-12-16 14:44:54 +01:00
const queryPage = extractQueryToInt(query.page)
2020-08-27 14:44:21 +02:00
2022-12-16 14:44:54 +01:00
this.currentPage = queryPage && this.currentPage !== queryPage
? queryPage
: 1
},
2020-08-27 14:44:21 +02:00
2022-12-16 14:44:54 +01:00
countActiveFilters () {
const query = this.$route.query as SearchUrl
2020-08-27 14:44:21 +02:00
2022-12-16 14:44:54 +01:00
let count = 0
2021-05-03 16:52:54 +02:00
2022-12-16 14:44:54 +01:00
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++
2022-12-28 13:41:10 +01:00
if (query.resultType) count++
2022-12-16 14:44:54 +01:00
if (Array.isArray(query.tagsAllOf) && query.tagsAllOf.length !== 0) count++
if (Array.isArray(query.tagsOneOf) && query.tagsOneOf.length !== 0) count++
2021-07-28 11:22:42 +02:00
2022-12-16 14:44:54 +01:00
return count
2020-08-27 14:44:21 +02:00
},
buildVideoSearchQuery () {
2022-12-16 14:44:54 +01:00
const query = this.$route.query as SearchUrl
2022-12-28 13:41:10 +01:00
const resultsPerPage = this.getResultTypeFilter() === 'videos'
? this.filteredTypeResultsPerPage
: this.summaryResultsCount.videos
const { start, count } = pageToAPIParams(this.currentPage, resultsPerPage)
2022-12-16 14:44:54 +01:00
const { durationMin, durationMax } = durationRangeToAPIParams(query.durationRange)
const { startDate, endDate } = publishedDateRangeToAPIParams(query.publishedDateRange)
2020-08-27 14:44:21 +02:00
2020-09-23 11:17:43 +02:00
const boostLanguages = this.boostLanguagesQuery
2022-12-16 14:44:54 +01:00
let sort: string
if (query.sort === '-matched') sort = '-matched'
else if (query.sort === '-createdAt') sort = '-publishedAt'
else if (query.sort === 'createdAt') sort = 'publishedAt'
2020-08-27 14:44:21 +02:00
return {
2022-12-16 14:44:54 +01:00
search: query.search,
2020-08-27 14:44:21 +02:00
durationMin,
durationMax,
startDate,
endDate,
2020-09-23 11:17:43 +02:00
boostLanguages,
2022-12-16 14:44:54 +01:00
nsfw: query.nsfw || false,
2020-08-27 14:44:21 +02:00
2022-12-16 14:44:54 +01:00
categoryOneOf: extractQueryToIntArray(query.categoryOneOf),
licenceOneOf: extractQueryToIntArray(query.licenceOneOf),
languageOneOf: extractQueryToStringArray(query.languageOneOf),
2020-08-27 14:44:21 +02:00
2022-12-16 14:44:54 +01:00
tagsOneOf: extractTagsFromQuery(query.tagsOneOf).map(t => t.text),
tagsAllOf: extractTagsFromQuery(query.tagsAllOf).map(t => t.text),
2020-08-27 14:44:21 +02:00
2022-12-16 14:44:54 +01:00
isLive: extractQueryToBoolean(query.isLive),
2021-05-03 16:52:54 +02:00
2022-12-16 14:44:54 +01:00
host: query.host || undefined,
2021-07-28 11:22:42 +02:00
2020-08-27 14:44:21 +02:00
start,
count,
2022-12-16 14:44:54 +01:00
sort
2020-08-27 14:44:21 +02:00
} as VideosSearchQuery
},
buildChannelSearchQuery () {
2022-12-16 14:44:54 +01:00
const query = this.$route.query as SearchUrl
2020-08-27 14:44:21 +02:00
2022-12-28 13:41:10 +01:00
const resultsPerPage = this.getResultTypeFilter() === 'channels'
? this.filteredTypeResultsPerPage
: this.summaryResultsCount.channels
const { start, count } = pageToAPIParams(this.currentPage, resultsPerPage)
2020-09-02 11:37:02 +02:00
2020-08-27 14:44:21 +02:00
return {
2022-12-16 14:44:54 +01:00
search: query.search,
host: query.host || undefined,
2020-08-27 14:44:21 +02:00
start,
2022-12-16 14:44:54 +01:00
sort: query.sort,
2020-08-27 14:44:21 +02:00
count
} as VideoChannelsSearchQuery
},
2021-06-24 16:53:43 +02:00
buildPlaylistSearchQuery () {
2022-12-16 14:44:54 +01:00
const query = this.$route.query as SearchUrl
2022-12-28 13:41:10 +01:00
const resultsPerPage = this.getResultTypeFilter() === 'playlists'
? this.filteredTypeResultsPerPage
: this.summaryResultsCount.playlists
const { start, count } = pageToAPIParams(this.currentPage, resultsPerPage)
2021-06-24 16:53:43 +02:00
return {
2022-12-16 14:44:54 +01:00
search: query.search,
host: query.host || undefined,
2021-06-24 16:53:43 +02:00
start,
2022-12-16 14:44:54 +01:00
sort: query.sort,
2021-06-24 16:53:43 +02:00
count
} as PlaylistsSearchQuery
},
2023-11-10 14:14:57 +01:00
searchVideos (): Promise<ResultList<APIVideo>> {
2022-12-28 13:41:10 +01:00
if (this.isVideoSearchDisabled()) {
return Promise.resolve({ total: 0, data: [] })
}
2020-08-27 14:44:21 +02:00
const query = this.buildVideoSearchQuery()
return searchVideos(query)
},
2023-11-10 14:14:57 +01:00
searchChannels (): Promise<ResultList<APIVideoChannel>> {
2022-12-28 13:41:10 +01:00
if (this.isChannelSearchDisabled()) {
2020-08-27 14:44:21 +02:00
return Promise.resolve({ data: [], total: 0 })
}
const query = this.buildChannelSearchQuery()
return searchVideoChannels(query)
},
2023-11-10 14:14:57 +01:00
searchPlaylists (): Promise<ResultList<APIPlaylist>> {
2022-12-28 13:41:10 +01:00
if (this.isPlaylistSearchDisabled()) {
2021-06-24 16:53:43 +02:00
return Promise.resolve({ data: [], total: 0 })
}
2022-12-28 14:27:28 +01:00
const query = this.buildPlaylistSearchQuery()
2021-06-24 16:53:43 +02:00
return searchVideoPlaylists(query)
},
2022-12-28 13:41:10 +01:00
// ---------------------------------------------------------------------------
2020-08-27 14:44:21 +02:00
2022-12-28 13:41:10 +01:00
getMaxPage () {
const resultType = (this.$route.query as SearchUrl).resultType
2021-06-24 16:53:43 +02:00
2022-12-28 13:41:10 +01:00
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)
2020-08-27 14:44:21 +02:00
// Limit to 10 pages
2022-12-28 13:41:10 +01:00
return Math.min(10, maxPage)
2020-08-27 14:44:21 +02:00
},
buildPages () {
this.pages = []
for (let i = 1; i <= this.getMaxPage(); i++) {
this.pages.push(i)
}
},
toggleFilters () {
this.displayFilters = !this.displayFilters
},
2022-12-28 13:41:10 +01:00
// ---------------------------------------------------------------------------
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
}
2022-12-16 14:44:54 +01:00
2021-07-28 11:22:42 +02:00
// We can search against host for playlists and channels
2022-12-28 13:41:10 +01:00
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
}
2022-12-28 13:41:10 +01:00
// We can search against host for playlists and channels
if (host) return this.countActiveFilters() > 1
2021-07-28 11:22:42 +02:00
2020-08-27 14:44:21 +02:00
return this.countActiveFilters() > 0
}
}
})
</script>
2022-02-23 15:21:02 +01:00
2022-12-16 14:44:54 +01:00
<style scoped lang="scss">
2022-02-23 15:21:02 +01:00
@use 'sass:math';
@import '../scss/_variables';
2022-12-16 14:44:54 +01:00
@import '../scss/bootstrap-mixins';
2022-02-23 15:21:02 +01:00
2022-12-16 14:44:54 +01:00
.search-input {
max-width: 700px;
2022-02-23 15:21:02 +01:00
margin: auto;
}
2022-12-16 14:44:54 +01:00
.warning-button {
background: transparent;
border: none;
position: absolute;
top: 10px;
right: 10px;
2022-02-23 15:21:02 +01:00
2022-12-16 14:44:54 +01:00
svg {
width: 30px;
height: 30px;
2022-02-23 15:21:02 +01:00
}
2022-12-16 14:44:54 +01:00
&:hover {
opacity: 0.8;
2022-02-23 15:21:02 +01:00
}
}
.filters-button {
position: relative;
2022-12-16 14:44:54 +01:00
padding-left: 1rem;
padding-right: 2rem;
2022-02-23 15:21:02 +01:00
.arrow-down {
$size: 4px;
position: absolute;
right: 0;
top: 50%;
margin-top: math.div(-$size, 2);
2022-12-16 14:44:54 +01:00
margin-left: 1.5rem;
margin-right: 0.75rem;
2022-02-23 15:21:02 +01:00
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;
2022-12-16 14:44:54 +01:00
flex-wrap: wrap;
2022-02-23 15:21:02 +01:00
2022-12-16 14:44:54 +01:00
> * {
@include margin-top(2rem);
}
}
2022-02-23 15:21:02 +01:00
2022-12-16 14:44:54 +01:00
.results {
@include margin(2rem 0);
}
2022-02-23 15:21:02 +01:00
2022-12-16 14:44:54 +01:00
.results-summary {
@include margin-bottom(3rem);
font-weight: $font-semibold;
text-align: center;
2022-02-23 15:21:02 +01:00
}
2022-12-28 13:41:10 +01:00
h2 {
@include font-size(1.125rem);
}
2022-02-23 15:21:02 +01:00
</style>