1343 lines
37 KiB
Vue
1343 lines
37 KiB
Vue
<template>
|
|
<div>
|
|
<my-header
|
|
:index-name="indexName"
|
|
:small-format="searchDone"
|
|
/>
|
|
<div
|
|
v-if="searchDone"
|
|
class="header-left-logo"
|
|
>
|
|
<img
|
|
src="/img/peertube-logo.svg"
|
|
alt=""
|
|
>
|
|
</div>
|
|
|
|
<main>
|
|
<form
|
|
id="search-anchor"
|
|
class="search-container"
|
|
:class="{ 'search-container-small': !searchDone }"
|
|
role="search"
|
|
onsubmit="return false;"
|
|
>
|
|
<input
|
|
v-model="formSearch"
|
|
:placeholder="inputPlaceholder"
|
|
autofocus
|
|
type="text"
|
|
name="search"
|
|
autocapitalize="off"
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
maxlength="1024"
|
|
>
|
|
|
|
<button @click="doNewSearch()">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<circle
|
|
cx="11"
|
|
cy="11"
|
|
r="8"
|
|
/>
|
|
<line
|
|
x1="21"
|
|
y1="21"
|
|
x2="16.65"
|
|
y2="16.65"
|
|
/>
|
|
</svg>
|
|
|
|
<template v-if="formSearch">
|
|
{{ $gettext('Go!') }}
|
|
</template>
|
|
<template v-else>
|
|
{{ $gettext('Explore!') }}
|
|
</template>
|
|
</button>
|
|
</form>
|
|
|
|
<h3
|
|
v-if="!searchDone"
|
|
v-translate="{ instancesCount: instancesCount, indexedInstancesUrl: indexedInstancesUrl, indexName: indexName }"
|
|
:class="{ 'none-opacity': !instancesCount }"
|
|
>
|
|
Search for your favorite videos, channels and playlists on <a
|
|
href="%{indexedInstancesUrl}"
|
|
target="_blank"
|
|
>%{instancesCount} PeerTube websites</a> indexed by %{indexName}!
|
|
</h3>
|
|
|
|
<search-warning
|
|
class="search-warning"
|
|
:index-name="indexName"
|
|
:highlight="!searchDone"
|
|
/>
|
|
|
|
<div
|
|
v-if="searchDone"
|
|
class="results"
|
|
>
|
|
<div class="filters">
|
|
<div class="button-rows">
|
|
<button
|
|
class="filters-button peertube-button"
|
|
@click="toggleFilters()"
|
|
>
|
|
{{ $gettext('Filters') }}
|
|
|
|
<span v-if="activeFilters">({{ activeFilters }})</span>
|
|
|
|
<div class="arrow-down" />
|
|
</button>
|
|
|
|
<div class="sort-select">
|
|
<label
|
|
v-translate
|
|
for="sort"
|
|
>Sort by:</label>
|
|
<div class="sort-select select-container">
|
|
<select
|
|
id="sort"
|
|
v-model="formSort"
|
|
name="sort"
|
|
>
|
|
<option
|
|
v-translate
|
|
value="-match"
|
|
>
|
|
Best match
|
|
</option>
|
|
<option
|
|
v-translate
|
|
value="-publishedAt"
|
|
>
|
|
Most recent
|
|
</option>
|
|
<option
|
|
v-translate
|
|
value="publishedAt"
|
|
>
|
|
Least recent
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form
|
|
v-if="displayFilters"
|
|
class="filters-content"
|
|
>
|
|
<div class="form-group small-height">
|
|
<div class="radio-label label-container">
|
|
<label v-translate>Display sensitive content</label>
|
|
|
|
<button
|
|
v-if="formNSFW !== undefined"
|
|
v-translate
|
|
class="reset-button"
|
|
@click="resetField('nsfw')"
|
|
>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
|
|
<div class="radio-container">
|
|
<input
|
|
id="sensitiveContentYes"
|
|
v-model="formNSFW"
|
|
type="radio"
|
|
name="sensitiveContent"
|
|
value="both"
|
|
>
|
|
<label
|
|
v-translate
|
|
for="sensitiveContentYes"
|
|
class="radio"
|
|
>Yes</label>
|
|
</div>
|
|
|
|
<div class="radio-container">
|
|
<input
|
|
id="sensitiveContentNo"
|
|
v-model="formNSFW"
|
|
type="radio"
|
|
name="sensitiveContent"
|
|
value="false"
|
|
>
|
|
<label
|
|
v-translate
|
|
for="sensitiveContentNo"
|
|
class="radio"
|
|
>No</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group small-height">
|
|
<div class="radio-label label-container">
|
|
<label v-translate>Display only</label>
|
|
|
|
<button
|
|
v-if="formIsLive !== undefined"
|
|
v-translate
|
|
class="reset-button"
|
|
@click="resetField('isLive')"
|
|
>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
|
|
<div class="radio-container">
|
|
<input
|
|
id="isLiveYes"
|
|
v-model="formIsLive"
|
|
type="radio"
|
|
name="isLive"
|
|
value="both"
|
|
>
|
|
<label
|
|
v-translate
|
|
for="isLiveYes"
|
|
class="radio"
|
|
>Live videos</label>
|
|
</div>
|
|
|
|
<div class="radio-container">
|
|
<input
|
|
id="isLiveNo"
|
|
v-model="formIsLive"
|
|
type="radio"
|
|
name="isLive"
|
|
value="false"
|
|
>
|
|
<label
|
|
v-translate
|
|
for="isLiveNo"
|
|
class="radio"
|
|
>VOD videos</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="radio-label label-container">
|
|
<label v-translate>Published date</label>
|
|
|
|
<button
|
|
v-if="formPublishedDateRange !== undefined"
|
|
v-translate
|
|
class="reset-button"
|
|
@click="resetField('publishedDateRange')"
|
|
>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
v-for="date in publishedDateRanges"
|
|
:key="date.id"
|
|
class="radio-container"
|
|
>
|
|
<input
|
|
:id="date.id"
|
|
v-model="formPublishedDateRange"
|
|
type="radio"
|
|
name="publishedDateRange"
|
|
:value="date.id"
|
|
>
|
|
<label
|
|
:for="date.id"
|
|
class="radio"
|
|
>{{ date.label }}</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="radio-label label-container">
|
|
<label v-translate>Duration</label>
|
|
<button
|
|
v-if="formDurationRange !== undefined"
|
|
v-translate
|
|
class="reset-button"
|
|
@click="resetField('durationRange')"
|
|
>
|
|
Reset
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
v-for="duration in durationRanges"
|
|
:key="duration.id"
|
|
class="radio-container"
|
|
>
|
|
<input
|
|
:id="duration.id"
|
|
v-model="formDurationRange"
|
|
type="radio"
|
|
name="durationRange"
|
|
:value="duration.id"
|
|
>
|
|
<label
|
|
:for="duration.id"
|
|
class="radio"
|
|
>{{ duration.label }}</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label
|
|
v-translate
|
|
for="category"
|
|
>Category</label>
|
|
<button
|
|
v-if="formCategoryOneOf !== undefined"
|
|
v-translate
|
|
class="reset-button"
|
|
@click="resetField('categoryOneOf')"
|
|
>
|
|
Reset
|
|
</button>
|
|
|
|
<div class="select-container">
|
|
<select
|
|
id="category"
|
|
v-model="formCategoryOneOf"
|
|
name="category"
|
|
>
|
|
<option
|
|
v-translate
|
|
:value="undefined"
|
|
>
|
|
Display all categories
|
|
</option>
|
|
<option
|
|
v-for="category in videoCategories"
|
|
:key="category.id"
|
|
:value="category.id"
|
|
>
|
|
{{ category.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label
|
|
v-translate
|
|
for="licence"
|
|
>Licence</label>
|
|
<button
|
|
v-if="formLicenceOneOf !== undefined"
|
|
v-translate
|
|
class="reset-button"
|
|
@click="resetField('licenceOneOf')"
|
|
>
|
|
Reset
|
|
</button>
|
|
|
|
<div class="select-container">
|
|
<select
|
|
id="licence"
|
|
v-model="formLicenceOneOf"
|
|
name="licence"
|
|
>
|
|
<option
|
|
v-translate
|
|
:value="undefined"
|
|
>
|
|
Display all licenses
|
|
</option>
|
|
<option
|
|
v-for="licence in videoLicences"
|
|
:key="licence.id"
|
|
:value="licence.id"
|
|
>
|
|
{{ licence.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label
|
|
v-translate
|
|
for="language"
|
|
>Language</label>
|
|
<button
|
|
v-if="formLanguageOneOf !== undefined"
|
|
v-translate
|
|
class="reset-button"
|
|
@click="resetField('languageOneOf')"
|
|
>
|
|
Reset
|
|
</button>
|
|
|
|
<div class="select-container">
|
|
<select
|
|
id="language"
|
|
v-model="formLanguageOneOf"
|
|
name="language"
|
|
>
|
|
<option
|
|
v-translate
|
|
:value="undefined"
|
|
>
|
|
Display all languages
|
|
</option>
|
|
<option
|
|
v-for="language in videoLanguages"
|
|
:key="language.id"
|
|
:value="language.id"
|
|
>
|
|
{{ language.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label
|
|
v-translate
|
|
for="host"
|
|
>PeerTube instance</label>
|
|
|
|
<input
|
|
id="host"
|
|
v-model="formHost"
|
|
type="text"
|
|
name="host"
|
|
class="classic-input-text"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label
|
|
v-translate
|
|
for="tagsAllOf"
|
|
>All of these tags</label>
|
|
<button
|
|
v-if="formTagsAllOf.length !== 0"
|
|
v-translate
|
|
class="reset-button"
|
|
@click="resetField('tagsAllOf')"
|
|
>
|
|
Reset
|
|
</button>
|
|
|
|
<vue-tags-input
|
|
v-model="formTagAllOf"
|
|
:placeholder="tagsPlaceholder"
|
|
:tags="formTagsAllOf"
|
|
@tags-changed="newTags => formTagsAllOf = newTags"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label
|
|
v-translate
|
|
for="tagsOneOf"
|
|
>One of these tags</label>
|
|
<button
|
|
v-if="formTagsOneOf.length !== 0"
|
|
v-translate
|
|
class="reset-button"
|
|
@click="resetField('tagsOneOf')"
|
|
>
|
|
Reset
|
|
</button>
|
|
|
|
<vue-tags-input
|
|
v-model="formTagOneOf"
|
|
:placeholder="tagsPlaceholder"
|
|
:tags="formTagsOneOf"
|
|
@tags-changed="newTags => formTagsOneOf = newTags"
|
|
/>
|
|
</div>
|
|
|
|
<div class="button-block">
|
|
<input
|
|
class="peertube-button"
|
|
type="button"
|
|
:value="applyFiltersLabel"
|
|
@click="doNewSearch()"
|
|
>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div
|
|
id="results-anchor"
|
|
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 + '' }) }}
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
v-for="result in results"
|
|
:key="getResultKey(result)"
|
|
>
|
|
<video-result
|
|
v-if="isVideo(result)"
|
|
:video="result"
|
|
/>
|
|
|
|
<channel-result
|
|
v-else-if="isChannel(result)"
|
|
:channel="result"
|
|
/>
|
|
|
|
<playlist-result
|
|
v-else
|
|
:playlist="result"
|
|
/>
|
|
</div>
|
|
|
|
<pagination
|
|
v-model="currentPage"
|
|
:max-page="getMaxPage()"
|
|
:search-done="searchDone"
|
|
: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 { searchVideos, searchVideoChannels, searchVideoPlaylists } from '../shared/search'
|
|
import { getConfig } from '../shared/config'
|
|
import { pageToAPIParams, durationRangeToAPIParams, publishedDateRangeToAPIParams, extractTagsFromQuery } from '../shared/utils'
|
|
import { SearchUrl } from '../models'
|
|
import { EnhancedVideo } from '../../../server/types/video.model'
|
|
import { EnhancedVideoChannel } from '../../../server/types/channel.model'
|
|
import VueTagsInput from '@sipec/vue3-tags-input'
|
|
import Pagination from '../components/Pagination.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'
|
|
import { LocationQueryRaw } from 'vue-router'
|
|
|
|
export default defineComponent({
|
|
components: {
|
|
'my-header': Header,
|
|
'search-warning': SearchWarning,
|
|
'video-result': VideoResult,
|
|
'channel-result': ChannelResult,
|
|
'playlist-result': PlaylistResult,
|
|
'vue-tags-input': VueTagsInput,
|
|
'pagination': Pagination
|
|
},
|
|
|
|
data () {
|
|
return {
|
|
searchDone: false,
|
|
|
|
indexName: '',
|
|
instancesCount: 0,
|
|
indexedInstancesUrl: '',
|
|
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,
|
|
|
|
displayFilters: false,
|
|
oldQuery: '',
|
|
|
|
formSearch: '',
|
|
formSort: '-match',
|
|
|
|
formNSFW: undefined,
|
|
formHost: '',
|
|
formPublishedDateRange: undefined,
|
|
formDurationRange: undefined,
|
|
formCategoryOneOf: undefined,
|
|
formLicenceOneOf: undefined,
|
|
formLanguageOneOf: undefined,
|
|
|
|
formTagAllOf: '',
|
|
formTagOneOf: '',
|
|
formTagsAllOf: [],
|
|
formTagsOneOf: [],
|
|
|
|
formIsLive: undefined,
|
|
|
|
activeFilters: 0
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
applyFiltersLabel (): string {
|
|
return this.$gettext('Apply filters')
|
|
},
|
|
|
|
inputPlaceholder (): string {
|
|
return this.$gettext('Keyword, channel, video, playlist, etc.')
|
|
},
|
|
|
|
tagsPlaceholder (): string {
|
|
return this.$gettext('Add tag')
|
|
},
|
|
|
|
publishedDateRanges (): { id: string, label: string }[] {
|
|
return [
|
|
{
|
|
id: 'any_published_date',
|
|
label: this.$gettext('Any')
|
|
},
|
|
{
|
|
id: 'today',
|
|
label: this.$gettext('Today')
|
|
},
|
|
{
|
|
id: 'last_7days',
|
|
label: this.$gettext('Last 7 days')
|
|
},
|
|
{
|
|
id: 'last_30days',
|
|
label: this.$gettext('Last 30 days')
|
|
},
|
|
{
|
|
id: 'last_365days',
|
|
label: this.$gettext('Last 365 days')
|
|
}
|
|
]
|
|
},
|
|
|
|
durationRanges (): { id: string, label: string }[] {
|
|
return [
|
|
{
|
|
id: 'any_duration',
|
|
label: this.$gettext('Any')
|
|
},
|
|
{
|
|
id: 'short',
|
|
label: this.$gettext('Short (< 4 min)')
|
|
},
|
|
{
|
|
id: 'medium',
|
|
label: this.$gettext('Medium (4-10 min)')
|
|
},
|
|
{
|
|
id: 'long',
|
|
label: this.$gettext('Long (> 10 min)')
|
|
}
|
|
]
|
|
},
|
|
|
|
videoCategories (): { id: string, label: string }[] {
|
|
return [
|
|
{ id: '1', label: this.$gettext('Music') },
|
|
{ id: '2', label: this.$gettext('Films') },
|
|
{ id: '3', label: this.$gettext('Vehicles') },
|
|
{ id: '4', label: this.$gettext('Art') },
|
|
{ id: '5', label: this.$gettext('Sports') },
|
|
{ id: '6', label: this.$gettext('Travels') },
|
|
{ id: '7', label: this.$gettext('Gaming') },
|
|
{ id: '8', label: this.$gettext('People') },
|
|
{ id: '9', label: this.$gettext('Comedy') },
|
|
{ id: '10', label: this.$gettext('Entertainment') },
|
|
{ id: '11', label: this.$gettext('News & Politics') },
|
|
{ id: '12', label: this.$gettext('How To') },
|
|
{ id: '13', label: this.$gettext('Education') },
|
|
{ id: '14', label: this.$gettext('Activism') },
|
|
{ id: '15', label: this.$gettext('Science & Technology') },
|
|
{ id: '16', label: this.$gettext('Animals') },
|
|
{ id: '17', label: this.$gettext('Kids') },
|
|
{ id: '18', label: this.$gettext('Food') }
|
|
]
|
|
},
|
|
|
|
videoLicences (): { id: string, label: string }[] {
|
|
return [
|
|
{ id: '1', label: this.$gettext('Attribution') },
|
|
{ id: '2', label: this.$gettext('Attribution - Share Alike') },
|
|
{ id: '3', label: this.$gettext('Attribution - No Derivatives') },
|
|
{ id: '4', label: this.$gettext('Attribution - Non Commercial') },
|
|
{ id: '5', label: this.$gettext('Attribution - Non Commercial - Share Alike') },
|
|
{ id: '6', label: this.$gettext('Attribution - Non Commercial - No Derivatives') },
|
|
{ id: '7', label: this.$gettext('Public Domain Dedication') }
|
|
]
|
|
},
|
|
|
|
videoLanguages (): { id: string, label: string }[] {
|
|
return [
|
|
{ id: 'en', label: this.$gettext('English') },
|
|
{ id: 'fr', label: this.$gettext('Français') },
|
|
{ id: 'ja', label: this.$gettext('日本語') },
|
|
{ id: 'eu', label: this.$gettext('Euskara') },
|
|
{ id: 'ca', label: this.$gettext('Català') },
|
|
{ id: 'cs', label: this.$gettext('Čeština') },
|
|
{ id: 'eo', label: this.$gettext('Esperanto') },
|
|
{ id: 'el', label: this.$gettext('ελληνικά') },
|
|
{ id: 'de', label: this.$gettext('Deutsch') },
|
|
{ id: 'it', label: this.$gettext('Italiano') },
|
|
{ id: 'nl', label: this.$gettext('Nederlands') },
|
|
{ id: 'es', label: this.$gettext('Español') },
|
|
{ id: 'oc', label: this.$gettext('Occitan') },
|
|
{ id: 'gd', label: this.$gettext('Gàidhlig') },
|
|
{ id: 'zh', label: this.$gettext('简体中文(中国)') },
|
|
{ id: 'pt', label: this.$gettext('Português (Portugal)') },
|
|
{ id: 'sv', label: this.$gettext('svenska') },
|
|
{ id: 'pl', label: this.$gettext('Polski') },
|
|
{ id: 'fi', label: this.$gettext('suomi') },
|
|
{ id: 'ru', label: this.$gettext('русский') }
|
|
]
|
|
},
|
|
|
|
boostLanguagesQuery (): string[] {
|
|
const languages = new Set<string>()
|
|
|
|
for (const completeLanguage of navigator.languages) {
|
|
const language = completeLanguage.split('-')[0]
|
|
|
|
if (this.videoLanguages.find(vl => vl.id === language)) {
|
|
languages.add(language)
|
|
}
|
|
}
|
|
|
|
return Array.from(languages)
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
// For pagination change
|
|
$route() {
|
|
const urlPage = this.$route.query.page as string
|
|
|
|
const scrollToResults = urlPage && parseInt(urlPage) !== this.currentPage
|
|
|
|
this.loadUrl()
|
|
|
|
this.doSearch()
|
|
if (scrollToResults) this.scrollToResults()
|
|
},
|
|
|
|
formSort () {
|
|
this.updateUrl()
|
|
}
|
|
},
|
|
|
|
async mounted () {
|
|
const config = await getConfig()
|
|
|
|
this.instancesCount = config.indexedHostsCount
|
|
this.indexedInstancesUrl = config.indexedInstancesUrl
|
|
this.indexName = config.searchInstanceName
|
|
|
|
if (Object.keys(this.$route.query).length !== 0) {
|
|
// Speed up search done variable if the user came directly with a query
|
|
// So it does not see the homepage for a few ms
|
|
this.searchDone = true
|
|
this.loadUrl()
|
|
this.doSearch()
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
|
|
doNewSearch () {
|
|
this.currentPage = 1
|
|
this.channelsCount = null
|
|
this.videosCount = null
|
|
this.playlistsCount = null
|
|
this.resultsCount = null
|
|
|
|
this.updateUrl()
|
|
},
|
|
|
|
async doSearch () {
|
|
// First search, scroll to top
|
|
if (this.searchDone !== true) this.scrollToTop()
|
|
|
|
this.results = []
|
|
|
|
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)
|
|
|
|
if (this.formSort === '-match') {
|
|
this.results.sort((r1, r2) => {
|
|
if (r1.score < r2.score) return 1
|
|
else if (r1.score === r2.score) return 0
|
|
|
|
return -1
|
|
})
|
|
}
|
|
|
|
this.buildPages()
|
|
this.searchDone = true
|
|
} catch (err) {
|
|
console.error('Cannot do search.', err)
|
|
} finally {
|
|
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')
|
|
},
|
|
|
|
updateUrl () {
|
|
const query: SearchUrl = {
|
|
search: this.formSearch,
|
|
sort: this.formSort,
|
|
nsfw: this.formNSFW,
|
|
host: this.formHost,
|
|
isLive: this.formIsLive,
|
|
publishedDateRange: this.formPublishedDateRange,
|
|
durationRange: this.formDurationRange,
|
|
categoryOneOf: this.formCategoryOneOf,
|
|
licenceOneOf: this.formLicenceOneOf,
|
|
languageOneOf: this.formLanguageOneOf,
|
|
tagsAllOf: this.formTagsAllOf.map(t => t.text),
|
|
tagsOneOf: this.formTagsOneOf.map(t => t.text),
|
|
page: this.currentPage
|
|
}
|
|
|
|
this.$router.push({ path: '/search', query: query as LocationQueryRaw })
|
|
},
|
|
|
|
loadUrl () {
|
|
const query = this.$route.query as SearchUrl
|
|
|
|
if (query.search) this.formSearch = query.search
|
|
else this.formSearch = undefined
|
|
|
|
if (query.nsfw) this.formNSFW = query.nsfw
|
|
else this.formNSFW = undefined
|
|
|
|
if (query.publishedDateRange) this.formPublishedDateRange = query.publishedDateRange
|
|
else this.formPublishedDateRange = undefined
|
|
|
|
if (query.durationRange) this.formDurationRange = query.durationRange
|
|
else this.formDurationRange = undefined
|
|
|
|
if (query.categoryOneOf) this.formCategoryOneOf = query.categoryOneOf
|
|
else this.formCategoryOneOf = undefined
|
|
|
|
if (query.licenceOneOf) this.formLicenceOneOf = query.licenceOneOf
|
|
else this.formLicenceOneOf = undefined
|
|
|
|
if (query.languageOneOf) this.formLanguageOneOf = query.languageOneOf
|
|
else this.formLanguageOneOf = undefined
|
|
|
|
if (query.tagsAllOf) this.formTagsAllOf = extractTagsFromQuery(query.tagsAllOf)
|
|
else this.formTagsAllOf = []
|
|
|
|
if (query.tagsOneOf) this.formTagsOneOf = extractTagsFromQuery(query.tagsOneOf)
|
|
else this.formTagsOneOf = []
|
|
|
|
if (query.sort) this.formSort = query.sort
|
|
else this.formSort = '-match'
|
|
|
|
if (query.isLive) this.formIsLive = query.isLive
|
|
else this.formIsLive = undefined
|
|
|
|
if (query.host) this.formHost = query.host
|
|
else this.formHost = ''
|
|
|
|
if (query.page && this.currentPage !== query.page) {
|
|
this.currentPage = parseInt(query.page + '')
|
|
} else {
|
|
this.currentPage = 1
|
|
}
|
|
},
|
|
|
|
buildVideoSearchQuery () {
|
|
const { start, count } = pageToAPIParams(this.currentPage, this.resultsPerVideosPage)
|
|
const { durationMin, durationMax } = durationRangeToAPIParams(this.formDurationRange)
|
|
const { startDate, endDate } = publishedDateRangeToAPIParams(this.formPublishedDateRange)
|
|
|
|
const boostLanguages = this.boostLanguagesQuery
|
|
|
|
return {
|
|
search: this.formSearch,
|
|
|
|
durationMin,
|
|
durationMax,
|
|
|
|
startDate,
|
|
endDate,
|
|
|
|
boostLanguages,
|
|
|
|
nsfw: this.formNSFW || false,
|
|
|
|
categoryOneOf: this.formCategoryOneOf ? [ this.formCategoryOneOf ] : undefined,
|
|
licenceOneOf: this.formLicenceOneOf ? [ this.formLicenceOneOf ] : undefined,
|
|
languageOneOf: this.formLanguageOneOf ? [ this.formLanguageOneOf ] : undefined,
|
|
|
|
tagsOneOf: this.formTagsOneOf.map(t => t.text),
|
|
tagsAllOf: this.formTagsAllOf.map(t => t.text),
|
|
|
|
isLive: this.formIsLive !== undefined ? this.formIsLive : undefined,
|
|
|
|
host: this.formHost || undefined,
|
|
|
|
start,
|
|
count,
|
|
sort: this.formSort
|
|
} as VideosSearchQuery
|
|
},
|
|
|
|
buildChannelSearchQuery () {
|
|
const { start, count } = pageToAPIParams(this.currentPage, this.resultsPerChannelsPage)
|
|
|
|
let sort: string
|
|
if (this.formSort === '-matched') sort = '-matched'
|
|
else if (this.formSort === '-publishedAt') sort = '-createdAt'
|
|
else if (this.formSort === 'publishedAt') sort = 'createdAt'
|
|
|
|
return {
|
|
search: this.formSearch,
|
|
host: this.formHost || undefined,
|
|
start,
|
|
sort,
|
|
count
|
|
} as VideoChannelsSearchQuery
|
|
},
|
|
|
|
buildPlaylistSearchQuery () {
|
|
const { start, count } = pageToAPIParams(this.currentPage, this.resultsPerChannelsPage)
|
|
|
|
let sort: string
|
|
if (this.formSort === '-matched') sort = '-matched'
|
|
else if (this.formSort === '-publishedAt') sort = '-createdAt'
|
|
else if (this.formSort === 'publishedAt') sort = 'createdAt'
|
|
|
|
return {
|
|
search: this.formSearch,
|
|
host: this.formHost || undefined,
|
|
start,
|
|
sort,
|
|
count
|
|
} as PlaylistsSearchQuery
|
|
},
|
|
|
|
searchVideos (): Promise<ResultList<EnhancedVideo>> {
|
|
if (!this.hasStillMoreVideosResults()) {
|
|
return Promise.resolve({ data: [], total: this.videosCount })
|
|
}
|
|
|
|
const query = this.buildVideoSearchQuery()
|
|
|
|
return searchVideos(query)
|
|
},
|
|
|
|
searchChannels (): Promise<ResultList<EnhancedVideoChannel>> {
|
|
if (this.isChannelOrPlaylistSearchDisabled()) {
|
|
return Promise.resolve({ data: [], total: 0 })
|
|
}
|
|
|
|
if (!this.hasStillChannelsResult()) {
|
|
return Promise.resolve({ data: [], total: this.channelsCount })
|
|
}
|
|
|
|
const query = this.buildChannelSearchQuery()
|
|
|
|
return searchVideoChannels(query)
|
|
},
|
|
|
|
searchPlaylists (): Promise<ResultList<EnhancedPlaylist>> {
|
|
if (this.isChannelOrPlaylistSearchDisabled()) {
|
|
return Promise.resolve({ data: [], total: 0 })
|
|
}
|
|
|
|
if (!this.hasStillPlaylistsResult()) {
|
|
return Promise.resolve({ data: [], total: this.playlistsCount })
|
|
}
|
|
|
|
const query = this.buildChannelSearchQuery()
|
|
|
|
return searchVideoPlaylists(query)
|
|
},
|
|
|
|
hasStillChannelsResult () {
|
|
// Not searched yet
|
|
if (this.channelsCount === null) return true
|
|
|
|
return this.getChannelsMaxPage() >= this.currentPage
|
|
},
|
|
|
|
hasStillPlaylistsResult () {
|
|
// Not searched yet
|
|
if (this.playlistsCount === null) return true
|
|
|
|
return this.getPlaylistsMaxPage() >= this.currentPage
|
|
},
|
|
|
|
hasStillMoreVideosResults () {
|
|
// Not searched yet
|
|
if (this.videosCount === null) return true
|
|
|
|
return this.getVideosMaxPage() >= this.currentPage
|
|
},
|
|
|
|
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
|
|
},
|
|
|
|
resetField (field: string) {
|
|
if (field === 'nsfw') this.formNSFW = undefined
|
|
else if (field === 'publishedDateRange') this.formPublishedDateRange = undefined
|
|
else if (field === 'durationRange') this.formDurationRange = undefined
|
|
else if (field === 'categoryOneOf') this.formCategoryOneOf = undefined
|
|
else if (field === 'licenceOneOf') this.formLicenceOneOf = undefined
|
|
else if (field === 'languageOneOf') this.formLanguageOneOf = undefined
|
|
else if (field === 'isLive') this.formIsLive = undefined
|
|
else if (field === 'tagsAllOf') this.formTagsAllOf = []
|
|
else if (field === 'tagsOneOf') this.formTagsOneOf = []
|
|
else if (field === 'host') this.formHost = ''
|
|
},
|
|
|
|
countActiveFilters () {
|
|
let count = 0
|
|
|
|
if (this.formNSFW) count++
|
|
if (this.formHost) count++
|
|
if (this.formPublishedDateRange) count++
|
|
if (this.formDurationRange) count++
|
|
if (this.formCategoryOneOf) count++
|
|
if (this.formLicenceOneOf) count++
|
|
if (this.formLanguageOneOf) count++
|
|
if (this.formIsLive) count++
|
|
if (this.formTagsAllOf && this.formTagsAllOf.length !== 0) count++
|
|
if (this.formTagsOneOf && this.formTagsOneOf.length !== 0) count++
|
|
|
|
return count
|
|
},
|
|
|
|
isChannelOrPlaylistSearchDisabled () {
|
|
// We can search against host for playlists and channels
|
|
if (this.formHost) return this.countActiveFilters() > 1
|
|
|
|
return this.countActiveFilters() > 0
|
|
},
|
|
|
|
scrollToResults () {
|
|
const anchor = document.getElementById('results-anchor')
|
|
if (anchor) anchor.scrollIntoView()
|
|
},
|
|
|
|
scrollToTop () {
|
|
window.scrollTo(0, 0)
|
|
}
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@use 'sass:math';
|
|
|
|
@import '../scss/_variables';
|
|
|
|
main {
|
|
margin: auto;
|
|
}
|
|
|
|
h3 {
|
|
max-width: 600px;
|
|
text-align: center;
|
|
margin: 40px auto 100px auto;
|
|
font-weight: normal;
|
|
font-size: 16px;
|
|
|
|
a {
|
|
color: $orange-darken;
|
|
|
|
&:hover {
|
|
color: $orange;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: $small-screen) {
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
.search-container {
|
|
background-color: #fff;
|
|
border-radius: 2px;
|
|
position: relative;
|
|
height: 45px;
|
|
margin: auto;
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
|
|
input[type=text] {
|
|
background-color: transparent;
|
|
outline: none;
|
|
height: 35px;
|
|
font-size: 15px;
|
|
border: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
color: #000;
|
|
padding: 0 20px;
|
|
}
|
|
|
|
button {
|
|
border-top-right-radius: 2px;
|
|
border-bottom-right-radius: 2px;
|
|
cursor: pointer;
|
|
position: absolute;
|
|
right: 0;
|
|
background-color: $orange-darken;
|
|
border: 0;
|
|
color: #FFF;
|
|
height: 100%;
|
|
outline: 0;
|
|
font-size: 15px;
|
|
padding: 0 15px 0 10px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-width: 100px;
|
|
|
|
svg {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
&:hover {
|
|
background-color: $orange;
|
|
}
|
|
}
|
|
|
|
button + div {
|
|
margin-top: 10px;
|
|
font-size: 80%;
|
|
text-align: right;
|
|
}
|
|
}
|
|
|
|
.search-container-small {
|
|
max-width: 500px;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.header-left-logo {
|
|
position: relative;
|
|
display: block;
|
|
height: 0;
|
|
|
|
img {
|
|
position: absolute;
|
|
left: -50px;
|
|
top: 0;
|
|
width: 32px;
|
|
}
|
|
}
|
|
|
|
.results-summary,
|
|
.no-results {
|
|
margin-top: 50px;
|
|
}
|
|
|
|
.results {
|
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
|
padding-top: 20px;
|
|
}
|
|
|
|
.filters-content {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
margin-top: 20px;
|
|
|
|
.form-group:nth-child(2n-1) {
|
|
padding-right: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
width: 50%;
|
|
min-height: 80px;
|
|
display: inline-block;
|
|
margin: 10px 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
@media screen and (max-width: $small-view) {
|
|
.form-group {
|
|
width: 100%;
|
|
padding-right: 0;
|
|
}
|
|
}
|
|
|
|
.form-group > label,
|
|
.label-container label {
|
|
font-weight: $font-semibold;
|
|
}
|
|
|
|
.form-group > label,
|
|
.label-container {
|
|
display: inline-block;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.form-group.small-height {
|
|
min-height: 30px;
|
|
}
|
|
|
|
.radio-container {
|
|
display: inline-block;
|
|
margin: 0 10px 5px 0;
|
|
}
|
|
|
|
.button-block {
|
|
width: 100%;
|
|
text-align: right;
|
|
}
|
|
|
|
.radio-label {
|
|
display: block;
|
|
}
|
|
|
|
.reset-button {
|
|
background: none;
|
|
border: none;
|
|
font-weight: 600;
|
|
font-size: 11px;
|
|
opacity: 0.7;
|
|
margin-left: 5px;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
.filters-button {
|
|
position: relative;
|
|
padding-right: 25px;
|
|
|
|
.arrow-down {
|
|
$size: 4px;
|
|
|
|
position: absolute;
|
|
right: 0;
|
|
top: 50%;
|
|
margin-top: math.div(-$size, 2);
|
|
margin-right: 10px;
|
|
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;
|
|
|
|
.sort-select {
|
|
display: flex;
|
|
|
|
label {
|
|
color: $grey;
|
|
font-size: 14px;
|
|
min-width: fit-content;
|
|
margin: auto 5px auto 0;
|
|
}
|
|
|
|
.select-container {
|
|
max-width: 150px;
|
|
}
|
|
}
|
|
}
|
|
|
|
</style>
|