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

467 lines
13 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 class="results">
<div class="results-summary">
<span v-if="resultsCount === 0">
{{ $gettext('No results found.') }}
</span>
<span v-if="resultsCount">
{{ $ngettext('%{resultsCount} result found:', '%{resultsCount} results found:', resultsCount, { resultsCount: resultsCount.toLocaleString() }) }}
</span>
</div>
<div v-for="result in results" :key="getResultKey(result)">
<video-result v-if="isVideo(result)" :video="result" class="mb-4" />
<channel-result v-else-if="isChannel(result)" :channel="result" class="mb-4" />
<playlist-result v-else :playlist="result" class="mb-4" />
</div>
<pagination v-model="currentPage" :max-page="getMaxPage()" :searched="searched" :pages="pages" />
</div>
</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 { EnhancedVideo } from '../../../server/types/video.model'
import { EnhancedVideoChannel } from '../../../server/types/channel.model'
import Pagination from '../components/Pagination.vue'
import SearchInput from '../components/SearchInput.vue'
import SortButton from '../components/SortButton.vue'
import { VideoChannelsSearchQuery, ResultList, VideosSearchQuery } from '../../../PeerTube/shared/models'
import Nprogress from 'nprogress'
import { EnhancedPlaylist } 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: '',
results: [] as (EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist)[],
resultsCount: null as number,
channelsCount: null as number,
videosCount: null as number,
playlistsCount: null as number,
currentPage: 1,
pages: [],
resultsPerVideosPage: 10,
resultsPerChannelsPage: 3,
resultsPerPlaylistsPage: 3,
activeFilters: 0,
displayFilters: false,
oldQuery: ''
}
},
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()
}
},
async 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
})
})
const config = await getConfig()
this.indexName = config.searchInstanceName
if (Object.keys(this.$route.query).length !== 0) {
this.loadUrl()
this.doSearch()
}
},
methods: {
async doSearch () {
this.results = []
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.channelsCount = channelsResult.total
this.videosCount = videosResult.total
this.playlistsCount = playlistsResult.total
this.resultsCount = videosResult.total + channelsResult.total + playlistsResult.total
this.results = channelsResult.data
this.results = this.results.concat(playlistsResult.data)
this.results = this.results.concat(videosResult.data)
this.buildPages()
this.searched = true
} catch (err) {
console.error('Cannot do search.', err)
} finally {
this.searched = true
Nprogress.done()
}
},
isVideo (result: EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist): result is EnhancedVideo {
if ((result as EnhancedVideo).language) return true
return false
},
isPlaylist (result: EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist): result is EnhancedPlaylist {
if ((result as EnhancedPlaylist).videosLength !== undefined) return true
return false
},
isChannel (result: EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist): result is EnhancedVideoChannel {
if ((result as EnhancedVideoChannel).followingCount !== undefined) return true
return false
},
getResultKey (result: EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist) {
if (this.isVideo(result)) return (result as EnhancedVideo).uuid
if (this.isChannel(result)) return result.id + (result as EnhancedVideoChannel).host
if (this.isPlaylist(result)) return (result as EnhancedPlaylist).uuid
throw new Error('Unknown result')
},
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 (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 { start, count } = pageToAPIParams(this.currentPage, this.resultsPerVideosPage)
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 { start, count } = pageToAPIParams(this.currentPage, this.resultsPerChannelsPage)
return {
search: query.search,
host: query.host || undefined,
start,
sort: query.sort,
count
} as VideoChannelsSearchQuery
},
buildPlaylistSearchQuery () {
const query = this.$route.query as SearchUrl
const { start, count } = pageToAPIParams(this.currentPage, this.resultsPerChannelsPage)
return {
search: query.search,
host: query.host || undefined,
start,
sort: query.sort,
count
} as PlaylistsSearchQuery
},
searchVideos (): Promise<ResultList<EnhancedVideo>> {
const query = this.buildVideoSearchQuery()
return searchVideos(query)
},
searchChannels (): Promise<ResultList<EnhancedVideoChannel>> {
if (this.isChannelOrPlaylistSearchDisabled()) {
return Promise.resolve({ data: [], total: 0 })
}
const query = this.buildChannelSearchQuery()
return searchVideoChannels(query)
},
searchPlaylists (): Promise<ResultList<EnhancedPlaylist>> {
if (this.isChannelOrPlaylistSearchDisabled()) {
return Promise.resolve({ data: [], total: 0 })
}
const query = this.buildChannelSearchQuery()
return searchVideoPlaylists(query)
},
getChannelsMaxPage () {
return Math.ceil(this.channelsCount / this.resultsPerChannelsPage)
},
getPlaylistsMaxPage () {
return Math.ceil(this.playlistsCount / this.resultsPerPlaylistsPage)
},
getVideosMaxPage () {
return Math.ceil(this.videosCount / this.resultsPerVideosPage)
},
getMaxPage () {
// Limit to 10 pages
return Math.min(10, Math.max(this.getPlaylistsMaxPage(), this.getChannelsMaxPage(), this.getVideosMaxPage()))
},
buildPages () {
this.pages = []
for (let i = 1; i <= this.getMaxPage(); i++) {
this.pages.push(i)
}
},
toggleFilters () {
this.displayFilters = !this.displayFilters
},
isChannelOrPlaylistSearchDisabled () {
const query = this.$route.query as SearchUrl
// We can search against host for playlists and channels
if (query.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;
}
</style>