Refactor and redesign client
This commit is contained in:
parent
7f12b64bd6
commit
8ed5c72945
|
@ -42,7 +42,9 @@
|
|||
2,
|
||||
{
|
||||
"SwitchCase": 1,
|
||||
"MemberExpression": "off"
|
||||
"MemberExpression": "off",
|
||||
// https://github.com/eslint/eslint/issues/15299
|
||||
"ignoredNodes": ["PropertyDefinition"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/consistent-type-assertions": [
|
||||
|
@ -85,6 +87,8 @@
|
|||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-extraneous-class": "off",
|
||||
"@typescript-eslint/prefer-nullish-coalescing": "off",
|
||||
"@typescript-eslint/consistent-indexed-object-style": "off",
|
||||
// bugged but useful
|
||||
"@typescript-eslint/restrict-plus-operands": "off"
|
||||
},
|
||||
|
|
|
@ -12,7 +12,11 @@
|
|||
],
|
||||
"rules": {
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/html-self-closing": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
"vue/html-indent": "off",
|
||||
"vue/multiline-html-element-content-newline": "off"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
src/locale/**/*~
|
||||
dist/
|
||||
stats.html
|
||||
|
|
|
@ -12,25 +12,29 @@
|
|||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.20.5",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@sipec/vue3-tags-input": "^3.0.4",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.1",
|
||||
"@typescript-eslint/parser": "^5.12.1",
|
||||
"@vitejs/plugin-vue": "^2.2.2",
|
||||
"@vue/eslint-config-typescript": "^10.0.0",
|
||||
"axios": "^0.26.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"axios": "^1.2.1",
|
||||
"bootstrap": "^5.2.3",
|
||||
"eslint": "^8.9.0",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"markdown-it": "^12.3.2",
|
||||
"eslint-plugin-vue": "^9.8.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"rollup-plugin-visualizer": "^5.8.3",
|
||||
"sass": "^1.49.8",
|
||||
"typescript": "~4.5.5",
|
||||
"vite": "^2.8.4",
|
||||
"typescript": "~4.9.4",
|
||||
"vite": "^4.0.1",
|
||||
"vue": "^3.2.31",
|
||||
"vue-matomo": "^4.1.0",
|
||||
"vue-router": "^4.0.12",
|
||||
"vue-tsc": "^0.31.4",
|
||||
"vue3-gettext": "2.2.0-alpha.1"
|
||||
"vue-tsc": "^1.0.13",
|
||||
"vue3-gettext": "2.3.4"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 15 KiB |
|
@ -1,42 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" viewBox="2799 -911 16 22" version="1.1" id="svg13" sodipodi:docname="logo.svg" width="16" height="22" inkscape:version="0.92.2 5c3e80d, 2017-08-06">
|
||||
<metadata id="metadata17">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1916" inkscape:window-height="1040" id="namedview15" showgrid="false" inkscape:zoom="29.790476" inkscape:cx="-1.1827326" inkscape:cy="12.088" inkscape:window-x="0" inkscape:window-y="18" inkscape:window-maximized="0" inkscape:current-layer="svg13"/>
|
||||
<defs id="defs4">
|
||||
<style id="style2">
|
||||
.cls-3 {
|
||||
fill: #211f20;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #737373;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #f1680d;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: rgba(255, 255, 255, 0);
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Artboard_1" data-name="Artboard – 1" class="cls-1" transform="translate(0.03356777,-1.9929667)">
|
||||
<g id="Symbol_3_1" data-name="Symbol 3 – 1" transform="translate(2759,-975)">
|
||||
<g id="Group_44" data-name="Group 44" transform="translate(0,2.333)">
|
||||
<path id="Path_4" data-name="Path 4" class="cls-3" d="m -949,-500 v 10.667 l 8,-5.333" transform="translate(989,564)" inkscape:connector-curvature="0" style="fill:#211f20"/>
|
||||
<path id="Path_5" data-name="Path 5" class="cls-4" d="m -949,-500 v 10.667 l 8,-5.333" transform="translate(989,574.667)" inkscape:connector-curvature="0" style="fill:#737373"/>
|
||||
<path id="Path_6" data-name="Path 6" class="cls-5" d="m -949,-500 v 10.667 l 8,-5.333" transform="translate(997,569.333)" inkscape:connector-curvature="0" style="fill:#f1680d"/>
|
||||
<path id="Path_7" data-name="Path 7" class="cls-6" d="M 0,0 V 10.667 L 8,5.333 Z" transform="rotate(180,24,40)" inkscape:connector-curvature="0" style="fill:rgba(255,255,255,0)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
@ -1,34 +1,20 @@
|
|||
<template>
|
||||
<span class="actor">
|
||||
<img v-if="avatarUrl && !avatarError" :src="avatarUrl" alt="" :class="{ account: isAccount }" @error="setAvatarError()" />
|
||||
|
||||
<a
|
||||
:href="actor.url"
|
||||
rel="nofollow noreferrer noopener"
|
||||
target="_blank"
|
||||
class="actor"
|
||||
:title="linkTitle"
|
||||
>
|
||||
<img
|
||||
v-if="avatarUrl && !avatarError"
|
||||
:src="avatarUrl"
|
||||
alt=""
|
||||
:class="{ account: isAccount }"
|
||||
@error="setAvatarError()"
|
||||
>
|
||||
|
||||
<strong>{{ actor.displayName }}</strong>
|
||||
|
||||
<span class="actor-handle">
|
||||
{{ actor.name }}@{{ actor.host }}
|
||||
:href="actor.url" :title="linkTitle"
|
||||
class="peertube-link wrap-text" rel="nofollow noreferrer noopener" target="_blank"
|
||||
>{{ actor.displayName }}</a>
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { AccountSummary, VideoChannelSummary } from '../../../PeerTube/shared/models'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
actor: Object as PropType<AccountSummary | VideoChannelSummary>,
|
||||
actor: Object,
|
||||
type: String
|
||||
},
|
||||
|
||||
|
@ -39,7 +25,7 @@
|
|||
},
|
||||
|
||||
computed: {
|
||||
avatarUrl (): string {
|
||||
avatarUrl () {
|
||||
const avatars = this.actor.avatars
|
||||
if (avatars.length === 0) return ''
|
||||
|
||||
|
@ -53,13 +39,13 @@
|
|||
return avatars[0].url
|
||||
},
|
||||
|
||||
linkTitle (): string {
|
||||
linkTitle () {
|
||||
if (this.type === 'channel') return this.$gettext('Go on this channel page')
|
||||
|
||||
return this.$gettext('Go on this account page')
|
||||
},
|
||||
|
||||
isAccount (): boolean {
|
||||
isAccount () {
|
||||
return this.type === 'account'
|
||||
}
|
||||
},
|
||||
|
@ -72,49 +58,24 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import '../scss/_variables';
|
||||
|
||||
.actor {
|
||||
font-size: inherit;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
$size: 20px;
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
width: $size;
|
||||
height: $size;
|
||||
min-width: $size;
|
||||
min-height: $size;
|
||||
margin-right: 5px;
|
||||
|
||||
&.account {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-handle {
|
||||
color: $grey;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
strong,
|
||||
.actor-handle {
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
.actor-handle {
|
||||
display: block;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,55 +1,42 @@
|
|||
<template>
|
||||
<div class="channel root-result">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
:href="channel.url"
|
||||
:title="discoverChannelMessage"
|
||||
class="avatar"
|
||||
>
|
||||
<img
|
||||
v-if="channel.avatar"
|
||||
:src="channel.avatar.url"
|
||||
alt=""
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
src="/img/default-avatar.png"
|
||||
alt=""
|
||||
>
|
||||
<div class="channel search-card-result">
|
||||
|
||||
<a class="image-container" target="_blank" rel="nofollow noreferrer noopener" :href="channel.url" :title="discoverChannelMessage">
|
||||
<img v-if="hasAvatar()" :src="getAvatarUrl()" alt="" />
|
||||
<img v-else src="/img/default-avatar.png" alt="" />
|
||||
</a>
|
||||
|
||||
<div class="information">
|
||||
<div class="title-block">
|
||||
<h5 class="title">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
:href="channel.url"
|
||||
:title="discoverChannelMessage"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<h4>
|
||||
<strong>
|
||||
<a target="_blank" rel="nofollow noreferrer noopener" class="wrap-text" :href="channel.url" :title="discoverChannelMessage">
|
||||
{{ channel.displayName }}
|
||||
</a>
|
||||
</h5>
|
||||
</strong>
|
||||
</h4>
|
||||
|
||||
<span class="handle">{{ channel.name }}@{{ channel.host }}</span>
|
||||
</div>
|
||||
<div class="small-separator"></div>
|
||||
|
||||
<div class="additional-information">
|
||||
<div class="followers-count">
|
||||
{{ followersCountMessage }}
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="description wrap-text mb-3"
|
||||
v-html="descriptionHTML"
|
||||
></div>
|
||||
<!-- eslint-enable -->
|
||||
|
||||
<div class="metadata">
|
||||
<div>
|
||||
<label>{{ $gettext('Channel created on platform') }}</label>
|
||||
|
||||
<a class="peertube-link" target="_blank" rel="nofollow noreferrer noopener" :href="platformUrl">{{ host }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
{{ channel.description }}
|
||||
</div>
|
||||
|
||||
<div class="button">
|
||||
<div class="button-link-container">
|
||||
<a
|
||||
class="button-link"
|
||||
rel="nofollow noreferrer noopener"
|
||||
class="peertube-button peertube-primary-button peertube-button-link"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
:href="channel.url"
|
||||
>
|
||||
{{ discoverChannelMessage }}
|
||||
|
@ -60,51 +47,89 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { VideoChannel } from '../../../PeerTube/shared/models'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
channel: Object as PropType<VideoChannel>
|
||||
channel: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
descriptionHTML: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
host (): string {
|
||||
host () {
|
||||
const url = this.channel.url
|
||||
|
||||
return new URL(url as string).host
|
||||
return new URL(url).host
|
||||
},
|
||||
|
||||
discoverChannelMessage (): string {
|
||||
return this.$gettextInterpolate(this.$gettext('Discover this channel on %{host}'), { host: this.host })
|
||||
platformUrl () {
|
||||
const url = this.channel.url
|
||||
|
||||
const parsed = new URL(url)
|
||||
|
||||
return parsed.protocol + '//' + parsed.host
|
||||
},
|
||||
|
||||
followersCountMessage (): string {
|
||||
const translated = this.$ngettext('%{ n } follower', '%{ n } followers', this.channel.followersCount)
|
||||
discoverChannelMessage () {
|
||||
return this.$gettext('Discover this channel on %{host}', { host: this.host })
|
||||
}
|
||||
},
|
||||
|
||||
return this.$gettextInterpolate(translated, { n: this.channel.followersCount })
|
||||
mounted () {
|
||||
this.lazyRenderMarkdown(this.channel.description)
|
||||
.then(html => this.descriptionHTML = html)
|
||||
},
|
||||
|
||||
methods: {
|
||||
hasAvatar () {
|
||||
if (this.channel.avatar) return true
|
||||
|
||||
if (!Array.isArray(this.channel.avatars)) return false
|
||||
|
||||
return this.channel.avatars.length !== 0
|
||||
},
|
||||
|
||||
getAvatarUrl () {
|
||||
if (!Array.isArray(this.channel.avatars)) {
|
||||
if (!this.channel.avatar) return ''
|
||||
|
||||
return this.channel.avatar.url
|
||||
}
|
||||
|
||||
if (this.channel.avatar.length === 0) return ''
|
||||
|
||||
const biggestAvatar = [ ...this.channel.avatars ].sort((a1, a2) => {
|
||||
if (a1.width < a2.width) return 1
|
||||
if (a1.width === a2.width) return 0
|
||||
return -1
|
||||
})[0]
|
||||
|
||||
return biggestAvatar.url
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import '../scss/_variables';
|
||||
|
||||
.channel {
|
||||
.avatar {
|
||||
width: $thumbnail-width;
|
||||
min-width: $thumbnail-width;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
$size: 140px;
|
||||
|
||||
.image-container {
|
||||
img {
|
||||
object-fit: cover;
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
min-width: 110px;
|
||||
min-height: 110px;
|
||||
max-width: $size;
|
||||
max-height: $size;
|
||||
min-width: $size;
|
||||
min-height: $size;
|
||||
box-shadow: 4px 4px 0 0 $beige-700;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,458 @@
|
|||
<template>
|
||||
<div class="filters-content">
|
||||
<div class="form-group small-height">
|
||||
<div class="radio-label label-container">
|
||||
<label>{{ $gettext('Display sensitive content') }}</label>
|
||||
|
||||
<button v-if="formNSFW !== undefined" class="reset-button" @click="resetField('nsfw')">
|
||||
{{ $gettext('Reset') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="peertube-radio-container">
|
||||
<input id="sensitiveContentYes" v-model="formNSFW" type="radio" name="sensitiveContent" value="both">
|
||||
<label for="sensitiveContentYes" class="radio">{{ $gettext('Yes') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="peertube-radio-container">
|
||||
<input id="sensitiveContentNo" v-model="formNSFW" type="radio" name="sensitiveContent" value="false">
|
||||
<label for="sensitiveContentNo" class="radio">{{ $gettext('No') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group small-height">
|
||||
<div class="radio-label label-container">
|
||||
<label>{{ $gettext('Display only') }}</label>
|
||||
|
||||
<button v-if="formIsLive !== undefined" class="reset-button" @click="resetField('isLive')">{{ $gettext('Reset') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="peertube-radio-container">
|
||||
<input id="isLiveYes" v-model="formIsLive" type="radio" name="isLive" value="both">
|
||||
<label for="isLiveYes" class="radio">{{ $gettext('Live videos') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="peertube-radio-container">
|
||||
<input id="isLiveNo" v-model="formIsLive" type="radio" name="isLive" value="false">
|
||||
<label for="isLiveNo" class="radio">{{ $gettext('VOD videos') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="radio-label label-container">
|
||||
<label>{{ $gettext('Published date') }}</label>
|
||||
|
||||
<button v-if="formPublishedDateRange !== undefined" class="reset-button" @click="resetField('publishedDateRange')">
|
||||
{{ $gettext('Reset') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="date in publishedDateRanges" :key="date.id" class="peertube-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>{{ $gettext('Duration') }}</label>
|
||||
<button v-if="formDurationRange !== undefined" class="reset-button" @click="resetField('durationRange')">
|
||||
{{ $gettext('Reset') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="duration in durationRanges" :key="duration.id" class="peertube-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 for="category">{{ $gettext('Category') }}</label>
|
||||
<button v-if="formCategoryOneOf !== undefined" class="reset-button" @click="resetField('categoryOneOf')">
|
||||
{{ $gettext('Reset') }}
|
||||
</button>
|
||||
|
||||
<div class="select-container">
|
||||
<select id="category" v-model="formCategoryOneOf" name="category">
|
||||
<option :value="undefined">
|
||||
{{ $gettext('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 for="licence">{{ $gettext('Licence') }}</label>
|
||||
<button v-if="formLicenceOneOf !== undefined" class="reset-button" @click="resetField('licenceOneOf')">
|
||||
{{ $gettext('Reset') }}
|
||||
</button>
|
||||
|
||||
<div class="select-container">
|
||||
<select id="licence" v-model="formLicenceOneOf" name="licence">
|
||||
<option :value="undefined">
|
||||
{{ $gettext('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 for="language">{{ $gettext('Language') }}</label>
|
||||
<button v-if="formLanguageOneOf !== undefined" class="reset-button" @click="resetField('languageOneOf')">
|
||||
{{ $gettext('Reset') }}
|
||||
</button>
|
||||
|
||||
<div class="select-container">
|
||||
<select id="language" v-model="formLanguageOneOf" name="language">
|
||||
<option :value="undefined">
|
||||
{{ $gettext('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 for="host">{{ $gettext('PeerTube instance') }}</label>
|
||||
|
||||
<input id="host" v-model="formHost" type="text" name="host" class="classic-input-text" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tagsAllOf">{{ $gettext('All of these tags') }}</label>
|
||||
<button v-if="formTagsAllOf.length !== 0" class="reset-button" @click="resetField('tagsAllOf')">
|
||||
{{ $gettext('Reset') }}
|
||||
</button>
|
||||
|
||||
<vue-tags-input
|
||||
v-model="formTagAllOf" :placeholder="tagsPlaceholder" :tags="formTagsAllOf"
|
||||
@tags-changed="newTags => formTagsAllOf = newTags"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tagsOneOf">{{ $gettext('One of these tags') }}</label>
|
||||
<button v-if="formTagsOneOf.length !== 0" class="reset-button" @click="resetField('tagsOneOf')">
|
||||
{{ $gettext('Reset') }}
|
||||
</button>
|
||||
|
||||
<vue-tags-input
|
||||
v-model="formTagOneOf" :placeholder="tagsPlaceholder" :tags="formTagsOneOf"
|
||||
@tags-changed="newTags => formTagsOneOf = newTags"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="button-block mt-3">
|
||||
<router-link class="peertube-button peertube-button-link peertube-primary-button" :to="{ path: '/search', query: { ...getUrlQuery() } }">
|
||||
{{ applyFiltersLabel }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import VueTagsInput from '@sipec/vue3-tags-input'
|
||||
import { SearchUrl } from '@/models/search-url.model'
|
||||
import { extractTagsFromQuery } from '@/shared/utils'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
'vue-tags-input': VueTagsInput,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
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('русский') }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.loadUrl()
|
||||
},
|
||||
|
||||
methods: {
|
||||
getUrlQuery (): SearchUrl {
|
||||
return {
|
||||
...this.$route.query,
|
||||
|
||||
nsfw: this.formNSFW,
|
||||
host: this.formHost || undefined,
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
||||
loadUrl () {
|
||||
const query = this.$route.query as SearchUrl
|
||||
|
||||
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.isLive) this.formIsLive = query.isLive
|
||||
else this.formIsLive = undefined
|
||||
|
||||
if (query.host) this.formHost = query.host
|
||||
else this.formHost = ''
|
||||
},
|
||||
|
||||
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 = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use 'sass:math';
|
||||
|
||||
@import '../scss/_variables';
|
||||
|
||||
.filters-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1.25rem;
|
||||
|
||||
.form-group:nth-child(2n-1) {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
width: 50%;
|
||||
min-height: 60px;
|
||||
display: inline-block;
|
||||
margin: 10px 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.peertube-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: 0.7rem;
|
||||
opacity: 0.7;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,84 +1,19 @@
|
|||
<template>
|
||||
<footer id="main-footer">
|
||||
<div class="header">
|
||||
<img
|
||||
src="/img/bottom-peertube-logo.svg"
|
||||
alt=""
|
||||
>
|
||||
<div class="text-center">
|
||||
<img src="/img/bottom-peertube-logo.svg" alt="PeerTube">
|
||||
|
||||
<div
|
||||
v-translate
|
||||
class="description"
|
||||
>
|
||||
A free software to take back control of your videos
|
||||
<div class="mt-3">
|
||||
{{ $gettext('A free software to take back control of your videos') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div>
|
||||
<div
|
||||
v-translate
|
||||
class="subtitle"
|
||||
>
|
||||
Open your own videos website with PeerTube!
|
||||
</div>
|
||||
<div class="d-flex mt-4 justify-content-center mb-5 fs-7">
|
||||
<a class="peertube-link text-center mx-3" href="https://joinpeertube.org" target="_blank">{{ $gettext('Learn more about PeerTube') }}</a>
|
||||
|
||||
<a
|
||||
v-translate
|
||||
target="_blank"
|
||||
href="https://docs.joinpeertube.org/#/install-any-os"
|
||||
>Install PeerTube</a>
|
||||
<a class="peertube-link text-center mx-3" href="https://framagit.org/framasoft/peertube/search-index/" target="_blank">{{ $gettext('Search Index source code') }}</a>
|
||||
|
||||
<a
|
||||
v-translate
|
||||
target="_blank"
|
||||
href="https://joinpeertube.org#what-is-peertube"
|
||||
>Why should I have my own PeerTube website?</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
v-translate
|
||||
class="subtitle"
|
||||
>
|
||||
Create an account to take back control of your videos
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-translate
|
||||
target="_blank"
|
||||
href="https://joinpeertube.org/instances"
|
||||
>Open an account on a PeerTube website</a>
|
||||
|
||||
<a
|
||||
v-translate
|
||||
target="_blank"
|
||||
href="https://docs.joinpeertube.org/#/use-library?id=playlist"
|
||||
>Create playlists</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="big-link">
|
||||
<a
|
||||
v-translate
|
||||
href="https://joinpeertube.org"
|
||||
target="_blank"
|
||||
> >> Check all guides on joinpeertube.org << </a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<a
|
||||
v-translate
|
||||
href="https://framagit.org/framasoft/peertube/search-index/"
|
||||
target="_blank"
|
||||
>Source code</a>
|
||||
|
||||
<a
|
||||
v-if="legalNoticesUrl"
|
||||
v-translate
|
||||
:href="legalNoticesUrl"
|
||||
target="_blank"
|
||||
>Legal notices</a>
|
||||
<a v-if="legalNoticesUrl" :href="legalNoticesUrl" target="_blank">{{ $gettext('Legal notices') }}</a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
@ -104,115 +39,18 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../scss/_variables';
|
||||
@import '../scss/bootstrap-mixins';
|
||||
|
||||
footer {
|
||||
width: $container-width;
|
||||
margin: auto;
|
||||
background-color: $orange-lighten;
|
||||
padding: 30px 100px 10px 100px;
|
||||
font-family: monospace;
|
||||
|
||||
.header {
|
||||
margin: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
> *:first-child {
|
||||
@include margin(2rem);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: $brown;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
&:first-child {
|
||||
margin-right: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $container-width) {
|
||||
flex-direction: column;
|
||||
|
||||
> div {
|
||||
margin-top: 50px;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.columns a,
|
||||
.big-link a {
|
||||
color: $orange;
|
||||
margin: 0 auto 20px auto;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
|
||||
&:hover {
|
||||
color: $orange-darken;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: $brown;
|
||||
font-size: 20px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.big-link {
|
||||
margin-top: 50px;
|
||||
|
||||
a {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 80px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
a {
|
||||
color: $orange;
|
||||
font-size: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $container-width) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
padding: 30px;
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,39 +4,29 @@
|
|||
<interface-language-dropdown class="interface-language-dropdown" />
|
||||
|
||||
<h1>
|
||||
<router-link to="/" :title="$gettext('Come back to homepage')">
|
||||
<span v-if="configLoaded && !titleImageUrl">{{ indexName }}</span>
|
||||
|
||||
<img
|
||||
class="title-image"
|
||||
:src="titleImageUrl"
|
||||
:alt="indexName"
|
||||
>
|
||||
<img v-else class="title-image" :src="titleImageUrl" :alt="indexName" />
|
||||
</router-link>
|
||||
</h1>
|
||||
|
||||
<template v-if="!smallFormat">
|
||||
<h4>
|
||||
<div v-translate>
|
||||
A search engine of <a
|
||||
href="https://joinpeertube.org"
|
||||
target="_blank"
|
||||
>PeerTube</a> videos, channels and playlists
|
||||
</div>
|
||||
<SafeHTML>
|
||||
{{
|
||||
$gettext('A search engine of <a class="peertube-link" href="https://joinpeertube.org" target="_blank">PeerTube</a> videos, channels and playlists', {}, true)
|
||||
}}
|
||||
</SafeHTML>
|
||||
|
||||
<div v-translate>
|
||||
Developed by <a
|
||||
href="https://framasoft.org"
|
||||
target="_blank"
|
||||
>Framasoft</a>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<SafeHTML>
|
||||
{{
|
||||
$gettext('Developed by <a class="peertube-link" href="https://framasoft.org" target="_blank">Framasoft</a>', {}, true)
|
||||
}}
|
||||
</SafeHTML>
|
||||
</h4>
|
||||
|
||||
<div class="search-home">
|
||||
<img
|
||||
v-if="searchImageUrl"
|
||||
:src="searchImageUrl"
|
||||
alt=""
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</header>
|
||||
</div>
|
||||
|
@ -62,7 +52,6 @@
|
|||
return {
|
||||
configLoaded: false,
|
||||
titleImageUrl: '',
|
||||
searchImageUrl: ''
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -73,10 +62,6 @@
|
|||
? buildApiUrl(config.searchInstanceNameImage)
|
||||
: ''
|
||||
|
||||
this.searchImageUrl = config.searchInstanceSearchImage
|
||||
? buildApiUrl(config.searchInstanceSearchImage)
|
||||
: buildApiUrl('/img/search-home.png')
|
||||
|
||||
this.configLoaded = true
|
||||
}
|
||||
})
|
||||
|
@ -84,14 +69,15 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../scss/_variables';
|
||||
@import '../scss/bootstrap-mixins.scss';
|
||||
|
||||
header {
|
||||
@include margin-bottom(3rem);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
font-family: monospace;
|
||||
|
||||
.interface-language-dropdown {
|
||||
width: 20px;
|
||||
|
@ -100,69 +86,32 @@
|
|||
top: 10px;
|
||||
}
|
||||
|
||||
.search-home {
|
||||
width: 300px;
|
||||
height: 250px;
|
||||
margin: 30px 0 0 0;
|
||||
|
||||
img {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 50px;
|
||||
@include font-size(3rem);
|
||||
|
||||
// Because it's loaded dynamically
|
||||
min-height: 70px;
|
||||
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title-image {
|
||||
max-width: 500px;
|
||||
width: 500px;
|
||||
height: 78px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@include font-size(1.25rem);
|
||||
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
line-height: 25px;
|
||||
line-height: inherit;
|
||||
margin: 1rem 0 0;
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
font-weight: $font-semibold;
|
||||
@include media-breakpoint-down(sm) {
|
||||
@include font-size(1rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-screen) {
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 15px !important;
|
||||
line-height: initial;
|
||||
|
||||
div:first-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 400px) {
|
||||
.search-home {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
padding: .5rem 0;
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
|
|
@ -1,74 +1,62 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="searchDone"
|
||||
class="pagination"
|
||||
>
|
||||
<router-link
|
||||
class="previous"
|
||||
:class="{ 'none-opacity': modelValue === 1 }"
|
||||
:to="{ query: buildPageUrlQuery(modelValue - 1) }"
|
||||
>
|
||||
<div v-if="searched && hasPages()" class="pagination">
|
||||
<router-link class="previous" :class="{ 'opacity-0': modelValue === 1 }" :to="{ query: { ...getUrlQuery(modelValue - 1) } }">
|
||||
{{ $gettext('Previous page') }}
|
||||
</router-link>
|
||||
|
||||
<div class="pages">
|
||||
<template
|
||||
v-for="page in pages"
|
||||
:key="page"
|
||||
>
|
||||
<router-link
|
||||
v-if="page !== modelValue"
|
||||
class="go-to-page"
|
||||
:to="{ query: buildPageUrlQuery(page) }"
|
||||
>
|
||||
<template v-for="page in pages" :key="page">
|
||||
<router-link v-if="page !== modelValue" class="go-to-page" :to="{ query: { ...getUrlQuery(page) } }">
|
||||
{{ page }}
|
||||
</router-link>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="current"
|
||||
>
|
||||
<div v-else class="current">
|
||||
{{ page }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
class="next"
|
||||
:class="{ 'none-opacity': modelValue >= maxPage }"
|
||||
:to="{ query: buildPageUrlQuery(+modelValue + 1) }"
|
||||
>
|
||||
<router-link class="next" :class="{ 'opacity-0': modelValue >= maxPage }" :to="{ query: { ...getUrlQuery(+modelValue + 1) } }">
|
||||
{{ $gettext('Next page') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { SearchUrl } from '@/models'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
maxPage: Number,
|
||||
searchDone: Boolean,
|
||||
searched: Boolean,
|
||||
modelValue: Number,
|
||||
pages: Array as () => number[]
|
||||
},
|
||||
|
||||
methods: {
|
||||
buildPageUrlQuery (page: number) {
|
||||
const query = this.$route.query
|
||||
getUrlQuery (page: number): SearchUrl {
|
||||
return {
|
||||
...this.$route.query,
|
||||
|
||||
return Object.assign({}, query, { page })
|
||||
page: page + ''
|
||||
}
|
||||
},
|
||||
|
||||
hasPages () {
|
||||
return this.pages.length !== 0
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../scss/_variables';
|
||||
@import '../scss/bootstrap-mixins';
|
||||
|
||||
.pagination {
|
||||
margin-top: 50px;
|
||||
@include margin-top(3rem);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
|
@ -81,7 +69,7 @@
|
|||
}
|
||||
|
||||
a {
|
||||
color: $orange;
|
||||
color: $orange-main;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
|
@ -98,7 +86,7 @@
|
|||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,73 +1,68 @@
|
|||
<template>
|
||||
<div class="playlist root-result">
|
||||
<div class="thumbnail">
|
||||
<a
|
||||
class="img"
|
||||
:title="watchMessage"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
:href="playlist.url"
|
||||
>
|
||||
<img
|
||||
:src="playlist.thumbnailUrl"
|
||||
alt=""
|
||||
>
|
||||
<div class="playlist search-card-result">
|
||||
<div class="image-container">
|
||||
<a class="img" :title="watchMessage" target="_blank" rel="nofollow noreferrer noopener" :href="playlist.url">
|
||||
<img :src="playlist.thumbnailUrl" alt="" :class="{ error: thumbnailError }" @error="setThumbnailError()">
|
||||
|
||||
<span class="videos-length">{{ videosLengthLabel }}</span>
|
||||
<div class="behind-1"></div>
|
||||
<div class="behind-2"></div>
|
||||
<div class="behind-3"></div>
|
||||
|
||||
<span class="videos-length">
|
||||
<svg width="10" height="13" viewBox="0 0 10 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.47955e-05 6.26032V0L4.75 3.13016L2.47955e-05 6.26032Z" fill="#212529" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.47955e-05 12.5206V6.26025L4.75 9.39041L2.47955e-05 12.5206Z" fill="#212529" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.75002 9.39045V3.13013L9.5 6.26029L4.75002 9.39045Z" fill="#212529" />
|
||||
</svg>
|
||||
|
||||
{{ videosLengthLabel }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="information">
|
||||
<h5 class="title">
|
||||
<a
|
||||
:title="watchMessage"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
:href="playlist.url"
|
||||
>{{ playlist.displayName }}</a>
|
||||
</h5>
|
||||
<div class="flex-grow-1">
|
||||
<h4>
|
||||
<strong>
|
||||
<a class="wrap-text" :title="watchMessage" target="_blank" rel="nofollow noreferrer noopener" :href="playlist.url">
|
||||
{{ playlist.displayName }}
|
||||
</a>
|
||||
</strong>
|
||||
</h4>
|
||||
|
||||
<div class="small-separator mb-3"></div>
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="description"
|
||||
v-html="renderMarkdown(playlist.description)"
|
||||
class="description wrap-text mb-3"
|
||||
v-html="descriptionHTML"
|
||||
/>
|
||||
<!-- eslint-enable -->
|
||||
|
||||
<div class="metadata">
|
||||
<div class="by-account">
|
||||
<label v-translate>Created by</label>
|
||||
<actor-miniature
|
||||
type="account"
|
||||
:actor="playlist.ownerAccount"
|
||||
/>
|
||||
<div>
|
||||
<label>{{ $gettext('Created by') }}</label>
|
||||
<ActorMiniature type="account" :actor="playlist.ownerAccount" />
|
||||
|
||||
<span class="secondary-color ms-1">{{ $gettext('and updated on') }}</span>
|
||||
|
||||
{{ updatedDate }}
|
||||
</div>
|
||||
|
||||
<div class="by-channel">
|
||||
<label v-translate>In</label>
|
||||
<div>
|
||||
<label>{{ $gettext('In channel') }}</label>
|
||||
|
||||
<actor-miniature
|
||||
type="channel"
|
||||
:actor="playlist.videoChannel"
|
||||
/>
|
||||
<ActorMiniature type="channel" :actor="playlist.videoChannel" />
|
||||
</div>
|
||||
|
||||
<div class="publishedAt">
|
||||
<label v-translate>Updated on</label>
|
||||
<div class="mb-3">
|
||||
<label>{{ $gettext('On platform') }}</label>
|
||||
|
||||
<div class="value">
|
||||
{{ updateDate }}
|
||||
</div>
|
||||
<a class="peertube-link" target="_blank" rel="nofollow noreferrer noopener" :href="platformUrl">{{ host }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button">
|
||||
<a
|
||||
class="button-link"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
:href="playlist.url"
|
||||
>
|
||||
<div class="button-link-container">
|
||||
<a class="peertube-button peertube-primary-button peertube-button-link" target="_blank" rel="nofollow noreferrer noopener" :href="playlist.url">
|
||||
{{ watchMessage }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -76,98 +71,146 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import ActorMiniature from './ActorMiniature.vue'
|
||||
import { VideoPlaylist } from '../../../PeerTube/shared/models'
|
||||
import { renderMarkdown } from '../shared/markdown-render'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
'actor-miniature': ActorMiniature
|
||||
ActorMiniature
|
||||
},
|
||||
|
||||
props: {
|
||||
playlist: Object as PropType<VideoPlaylist>
|
||||
playlist: Object
|
||||
},
|
||||
|
||||
computed: {
|
||||
host (): string {
|
||||
const url = this.playlist.url
|
||||
|
||||
return new URL(url as string).host
|
||||
},
|
||||
|
||||
updateDate (): string {
|
||||
return new Date(this.playlist.updatedAt).toLocaleDateString()
|
||||
},
|
||||
|
||||
watchMessage (): string {
|
||||
return this.$gettextInterpolate(this.$gettext('Watch the playlist on %{host}'), { host: this.host })
|
||||
},
|
||||
|
||||
videosLengthLabel (): string {
|
||||
return this.$gettextInterpolate(this.$gettext('%{videosLength} videos'), { videosLength: this.playlist.videosLength })
|
||||
data () {
|
||||
return {
|
||||
thumbnailError: false,
|
||||
descriptionHTML: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
host () {
|
||||
const url = this.playlist.url
|
||||
|
||||
return new URL(url).host
|
||||
},
|
||||
|
||||
platformUrl () {
|
||||
const url = this.playlist.url
|
||||
|
||||
const parsed = new URL(url)
|
||||
|
||||
return parsed.protocol + '//' + parsed.host
|
||||
},
|
||||
|
||||
updatedDate () {
|
||||
return new Date(this.playlist.updatedAt).toLocaleDateString()
|
||||
},
|
||||
|
||||
watchMessage () {
|
||||
return this.$gettext('Watch the playlist on %{host}', { host: this.host })
|
||||
},
|
||||
|
||||
videosLengthLabel () {
|
||||
return this.$ngettext('%{videosLength} video', '%{videosLength} videos', this.playlist.videosLength, { videosLength: this.playlist.videosLength })
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.lazyRenderMarkdown(this.playlist.description)
|
||||
.then(html => this.descriptionHTML = html)
|
||||
},
|
||||
|
||||
methods: {
|
||||
renderMarkdown(markdown: string) {
|
||||
return renderMarkdown(markdown)
|
||||
setThumbnailError () {
|
||||
this.thumbnailError = true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import '../scss/_variables';
|
||||
|
||||
.playlist {
|
||||
|
||||
.thumbnail {
|
||||
margin-right: 20px;
|
||||
|
||||
--thumbnail-width: #{$thumbnail-width};
|
||||
--thumbnail-height: #{$thumbnail-height};
|
||||
|
||||
.image-container {
|
||||
// For the videos length overlay
|
||||
.img {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: var(--thumbnail-width);
|
||||
height: var(--thumbnail-height);
|
||||
}
|
||||
|
||||
.behind-1,
|
||||
.behind-2,
|
||||
.behind-3 {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
border: 1px solid $beige-700;
|
||||
}
|
||||
|
||||
.behind-1 {
|
||||
z-index: 3;
|
||||
left: 8px;
|
||||
width: calc(100% - 3px);
|
||||
top: 5px;
|
||||
background-color: $beige-400;
|
||||
}
|
||||
|
||||
.behind-2 {
|
||||
z-index: 2;
|
||||
left: 14px;
|
||||
width: calc(100% - 3px);
|
||||
top: 11px;
|
||||
background-color: $beige-500;
|
||||
border: 1px solid $beige-700;
|
||||
}
|
||||
|
||||
.behind-3 {
|
||||
z-index: 1;
|
||||
left: 20px;
|
||||
width: calc(100% - 4px);
|
||||
top: 17px;
|
||||
background-color: $beige-700;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 275px;
|
||||
height: 147px;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
&.error {
|
||||
border: 1px solid #E5E5E5;
|
||||
}
|
||||
}
|
||||
|
||||
.videos-length {
|
||||
font-size: 0.875rem;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 15;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
justify-content: center;
|
||||
padding: 0 15px;
|
||||
height: 100%;
|
||||
|
||||
color: #fff;
|
||||
background-color: rgba(0,0,0,.7);
|
||||
background-color: rgba($beige-800, 0.8);
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
font-weight: $font-semibold;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
--thumbnail-width: calc(100% + 10px);
|
||||
--thumbnail-height: auto;
|
||||
|
||||
img {
|
||||
border-radius: 0;
|
||||
}
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { parseHTML, getChildNodes, getAttribute, getText, nodeIs, getClass } from '../shared/html-parser'
|
||||
|
||||
export default defineComponent({
|
||||
render () {
|
||||
const parsed = parseHTML(this.$slots.default()[0].children as string)
|
||||
const toRender = []
|
||||
|
||||
getChildNodes(parsed).forEach(node => {
|
||||
if (nodeIs(node, 'TEXT')) {
|
||||
toRender.push(getText(node))
|
||||
return
|
||||
}
|
||||
|
||||
if (nodeIs(node, 'STRONG')) {
|
||||
toRender.push(h('strong', getText(node)))
|
||||
return
|
||||
}
|
||||
|
||||
if (nodeIs(node, 'EM')) {
|
||||
toRender.push(h('em', getText(node)))
|
||||
return
|
||||
}
|
||||
|
||||
if (nodeIs(node, 'ROUTER-LINK')) {
|
||||
toRender.push(h(RouterLink, { to: getAttribute(node, 'to'), className: getClass(node) }, () => getText(node)))
|
||||
return
|
||||
}
|
||||
|
||||
if (nodeIs(node, 'BR')) {
|
||||
toRender.push(h('br'))
|
||||
return
|
||||
}
|
||||
|
||||
if (nodeIs(node, 'A')) {
|
||||
toRender.push(h('a', {
|
||||
target: getAttribute(node, 'target'),
|
||||
href: getAttribute(node, 'href'),
|
||||
rel: getAttribute(node, 'rel'),
|
||||
className: getAttribute(node, 'class')
|
||||
}, getText(node)))
|
||||
}
|
||||
})
|
||||
|
||||
return h('span', toRender)
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,142 @@
|
|||
<template>
|
||||
<div>
|
||||
<label for="search" :class="{ 'visually-hidden': !label }">{{ $gettext('My search') }}</label>
|
||||
|
||||
<div class="input-container">
|
||||
<input
|
||||
v-model="formSearch"
|
||||
:placeholder="inputPlaceholder"
|
||||
autofocus
|
||||
type="text"
|
||||
name="search"
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
maxlength="1024"
|
||||
@keydown.enter="search"
|
||||
>
|
||||
|
||||
<router-link class="peertube-button peertube-primary-button search-button" :to="{ path: '/search', query: { ...getUrlQuery() } }">
|
||||
<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>
|
||||
|
||||
<span v-if="formSearch">{{ $gettext('Search!') }}</span>
|
||||
<span v-else>{{ $gettext('Explore!') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { SearchUrl } from '@/models/search-url.model'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
label: Boolean
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
formSearch: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
inputPlaceholder (): string {
|
||||
return this.$gettext('Keyword, channel, video, playlist, etc.')
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.loadUrl()
|
||||
},
|
||||
|
||||
methods: {
|
||||
getUrlQuery (): SearchUrl {
|
||||
return { ...this.$route.query, search: this.formSearch }
|
||||
},
|
||||
|
||||
loadUrl () {
|
||||
const query = this.$route.query as SearchUrl
|
||||
|
||||
if (query.search) this.formSearch = query.search
|
||||
else this.formSearch = undefined
|
||||
},
|
||||
|
||||
search (event: Event) {
|
||||
event.preventDefault()
|
||||
|
||||
this.$router.push({ path: '/search', query: { ...this.getUrlQuery() } })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use 'sass:math';
|
||||
|
||||
@import '../scss/_variables';
|
||||
@import '../scss/bootstrap-mixins';
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
height: 45px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 1.125rem;
|
||||
display: block;
|
||||
font-weight: $font-bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
border: 0;
|
||||
color: #000;
|
||||
flex-grow: 1;
|
||||
padding: 0 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
text-decoration: none;
|
||||
border-radius: 0 2px 2px 0;
|
||||
height: 100%;
|
||||
padding: 0 1rem 0 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
svg {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,16 +1,19 @@
|
|||
<template>
|
||||
<div
|
||||
class="block-warning"
|
||||
:class="{ highlight: highlight }"
|
||||
>
|
||||
<img
|
||||
src="/img/sepia-warning.svg"
|
||||
alt=""
|
||||
>
|
||||
<div class="block-warning" :class="{ block: isBlockMode }">
|
||||
<img src="/img/sepia-warning.svg" alt="" />
|
||||
|
||||
<div v-translate>
|
||||
<strong>%{indexName}</strong> displays videos and channels that match your search but is not the publisher, nor the owner.
|
||||
If you notice any problems with a video, report it to the administrators on the PeerTube website where the video is published.
|
||||
<div>
|
||||
<SafeHTML>
|
||||
{{
|
||||
$gettext(
|
||||
'<strong>%{indexName}</strong> displays videos and channels that match your search but is not the publisher, nor the owner.',
|
||||
{ indexName: indexName },
|
||||
true
|
||||
)
|
||||
}}
|
||||
</SafeHTML>
|
||||
|
||||
{{ $gettext('If you notice any problems with a video, report it to the administrators on the PeerTube website where the video is published.') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -21,7 +24,7 @@
|
|||
export default defineComponent({
|
||||
props: {
|
||||
indexName: String,
|
||||
highlight: Boolean
|
||||
isBlockMode: Boolean
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -30,29 +33,26 @@
|
|||
@import '../scss/_variables';
|
||||
|
||||
.block-warning {
|
||||
font-style: italic;
|
||||
color: $orange;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px auto;
|
||||
|
||||
img {
|
||||
display: none;
|
||||
margin-right: 20px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
&.highlight {
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.block {
|
||||
max-width: 800px;
|
||||
color: $brown;
|
||||
border: 3px solid #f4af81;
|
||||
margin: auto;
|
||||
border: 3px solid $beige-700;
|
||||
border-radius: 10px;
|
||||
font-style: normal;
|
||||
padding: 10px 20px;
|
||||
padding: 0.75rem 1.25rem;
|
||||
|
||||
img {
|
||||
margin-right: 20px;
|
||||
height: 70px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<div class="root">
|
||||
<label class="d-none d-sm-inline" for="sort">{{ $gettext('Sort by:') }}</label>
|
||||
|
||||
<div class="peertube-button peertube-secondary-button">
|
||||
<select id="sort" v-model="formSort" name="sort">
|
||||
<option value="-match">{{ $gettext('Best match') }}</option>
|
||||
|
||||
<option value="-createdAt">{{ $gettext('Most recent') }}</option>
|
||||
|
||||
<option value="createdAt">{{ $gettext('Least recent') }}</option>
|
||||
</select>
|
||||
|
||||
<span class="focus"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { SearchUrl } from '@/models'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
data () {
|
||||
return {
|
||||
formSort: '-match'
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
formSort () {
|
||||
this.updateUrl()
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.loadUrl()
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateUrl () {
|
||||
this.$router.push({ path: '/search', query: { ...this.$route.query, sort: this.formSort }})
|
||||
},
|
||||
|
||||
loadUrl () {
|
||||
const query = this.$route.query as SearchUrl
|
||||
|
||||
if (query.sort) this.formSort = query.sort
|
||||
else this.formSort = '-match'
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use 'sass:math';
|
||||
|
||||
@import '../scss/_variables';
|
||||
|
||||
.peertube-secondary-button {
|
||||
margin-left: 1rem;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
select {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: $font-bold;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 2rem 0.5rem 1rem;
|
||||
|
||||
&:focus + span {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
border: 2px solid var(--select-focus);
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 5px;
|
||||
background-color: $main-font-color;
|
||||
clip-path: polygon(100% 0%, 0 0%, 50% 100%);
|
||||
display: inline-block;
|
||||
margin-left: 1.5rem;
|
||||
margin-right: 0.75rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(50% - 2px);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,98 +1,64 @@
|
|||
<template>
|
||||
<div class="video root-result">
|
||||
<div class="thumbnail">
|
||||
<a
|
||||
class="img"
|
||||
:title="watchVideoMessage"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
:href="video.url"
|
||||
>
|
||||
<img
|
||||
:src="getVideoThumbnailUrl()"
|
||||
alt=""
|
||||
:class="{ error: thumbnailError }"
|
||||
@error="setThumbnailError()"
|
||||
>
|
||||
<div class="video search-card-result">
|
||||
<div class="image-container">
|
||||
<a class="img" :title="watchVideoMessage" target="_blank" rel="nofollow noreferrer noopener" :href="video.url">
|
||||
<img :src="getVideoThumbnailUrl()" alt="" :class="{ error: thumbnailError }" @error="setThumbnailError()">
|
||||
|
||||
<span
|
||||
v-if="video.isLive"
|
||||
v-translate
|
||||
class="live-info"
|
||||
>LIVE</span>
|
||||
<span
|
||||
v-else
|
||||
class="duration"
|
||||
>{{ formattedDuration }}</span>
|
||||
<span v-if="video.isLive" class="live-info">{{ $gettext('LIVE') }}</span>
|
||||
<span v-else class="duration">{{ formattedDuration }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="information">
|
||||
<h5 class="title">
|
||||
<a
|
||||
:title="watchVideoMessage"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
:href="video.url"
|
||||
>{{ video.name }}</a>
|
||||
</h5>
|
||||
<div class="flex-grow-1">
|
||||
<h4>
|
||||
<strong>
|
||||
<a class="wrap-text" :title="watchVideoMessage" target="_blank" rel="nofollow noreferrer noopener" :href="video.url">
|
||||
{{ video.name }}
|
||||
</a>
|
||||
</strong>
|
||||
</h4>
|
||||
|
||||
<div class="small-separator mb-3"></div>
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="description"
|
||||
v-html="renderMarkdown(video.description)"
|
||||
/>
|
||||
class="description wrap-text mb-3"
|
||||
v-html="descriptionHTML"
|
||||
></div>
|
||||
<!-- eslint-enable -->
|
||||
|
||||
<div class="metadata">
|
||||
<div class="by-account">
|
||||
<label v-translate>Created by</label>
|
||||
<actor-miniature
|
||||
type="account"
|
||||
:actor="video.account"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>{{ $gettext('Published by') }}</label>
|
||||
<ActorMiniature type="account" :actor="video.account" />
|
||||
|
||||
<div class="by-channel">
|
||||
<label v-translate>In</label>
|
||||
<span class="secondary-color ms-1">on</span>
|
||||
|
||||
<actor-miniature
|
||||
type="channel"
|
||||
:actor="video.channel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="publishedAt">
|
||||
<label v-translate>On</label>
|
||||
|
||||
<div class="value">
|
||||
{{ publicationDate }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>{{ $gettext('In channel') }}</label>
|
||||
|
||||
<ActorMiniature type="channel" :actor="video.channel" />
|
||||
</div>
|
||||
|
||||
<div class="language">
|
||||
<label v-translate>Language</label>
|
||||
<div class="mb-3">
|
||||
<label>{{ $gettext('On platform') }}</label>
|
||||
|
||||
<div class="value">
|
||||
{{ video.language.label }}
|
||||
<a class="peertube-link" target="_blank" rel="nofollow noreferrer noopener" :href="platformUrl">{{ host }}</a>
|
||||
</div>
|
||||
|
||||
<div v-if="video.language.id">
|
||||
<label>{{ $gettext('Language:') }}</label>
|
||||
|
||||
<span>{{ video.language.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="video.tags && video.tags.length !== 0"
|
||||
class="tags"
|
||||
>
|
||||
<label v-translate>Tags</label>
|
||||
|
||||
<div class="value">
|
||||
{{ video.tags.join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button">
|
||||
<div class="button-link-container">
|
||||
<a
|
||||
class="button-link"
|
||||
class="peertube-button peertube-primary-button peertube-button-link"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
:href="video.url"
|
||||
|
@ -105,51 +71,64 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import { durationToString } from '@/shared/utils'
|
||||
import ActorMiniature from './ActorMiniature.vue'
|
||||
import { durationToString } from '../shared/utils'
|
||||
import { renderMarkdown } from '../shared/markdown-render'
|
||||
import { EnhancedVideo } from '../../../server/types/video.model'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
'actor-miniature': ActorMiniature
|
||||
ActorMiniature
|
||||
},
|
||||
|
||||
props: {
|
||||
video: Object as PropType<EnhancedVideo>
|
||||
video: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
thumbnailError: false
|
||||
thumbnailError: false,
|
||||
descriptionHTML: ''
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
host (): string {
|
||||
host () {
|
||||
const url = this.video.url
|
||||
|
||||
return new URL(url as string).host
|
||||
return new URL(url).host
|
||||
},
|
||||
|
||||
formattedDuration (): string {
|
||||
platformUrl () {
|
||||
const url = this.video.url
|
||||
|
||||
const parsed = new URL(url)
|
||||
|
||||
return parsed.protocol + '//' + parsed.host
|
||||
},
|
||||
|
||||
formattedDuration () {
|
||||
return durationToString(this.video.duration)
|
||||
},
|
||||
|
||||
windowWidth (): number {
|
||||
windowWidth () {
|
||||
return window.innerWidth
|
||||
},
|
||||
|
||||
publicationDate (): string {
|
||||
publicationDate () {
|
||||
return new Date(this.video.publishedAt).toLocaleDateString()
|
||||
},
|
||||
|
||||
watchVideoMessage (): string {
|
||||
return this.$gettextInterpolate(this.$gettext('Watch the video on %{host}'), { host: this.host })
|
||||
watchVideoMessage () {
|
||||
return this.$gettext('Watch the video on %{host}', { host: this.host })
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.lazyRenderMarkdown(this.video.description)
|
||||
.then(html => this.descriptionHTML = html)
|
||||
},
|
||||
|
||||
methods: {
|
||||
getVideoThumbnailUrl () {
|
||||
if (this.windowWidth >= 900) {
|
||||
|
@ -161,36 +140,28 @@
|
|||
|
||||
setThumbnailError () {
|
||||
this.thumbnailError = true
|
||||
},
|
||||
|
||||
renderMarkdown(markdown: string) {
|
||||
return renderMarkdown(markdown)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import '../scss/_variables';
|
||||
|
||||
.video {
|
||||
|
||||
.thumbnail {
|
||||
margin-right: 20px;
|
||||
|
||||
--thumbnail-width: #{$thumbnail-width};
|
||||
--thumbnail-height: #{$thumbnail-height};
|
||||
|
||||
.image-container {
|
||||
// For the duration overlay
|
||||
.img {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
img {
|
||||
background-color: #E5E5E5;
|
||||
width: var(--thumbnail-width);
|
||||
height: var(--thumbnail-height);
|
||||
border-radius: 3px;
|
||||
width: 275px;
|
||||
height: 147px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 4px 4px 0 0 $beige-700;
|
||||
|
||||
&.error {
|
||||
border: 1px solid #E5E5E5;
|
||||
|
@ -200,13 +171,14 @@
|
|||
.duration,
|
||||
.live-info {
|
||||
position: absolute;
|
||||
padding: 2px 5px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
border-radius: 3px;
|
||||
background-color: #000;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 2px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.duration {
|
||||
|
@ -217,15 +189,6 @@
|
|||
background-color: rgba(224, 8, 8 ,.8);
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
--thumbnail-width: calc(100% + 10px);
|
||||
--thumbnail-height: auto;
|
||||
|
||||
img {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<svg width="57" height="57" viewBox="0 0 57 57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="28.5" cy="28.5" r="28.5" fill="#F2690D" />
|
||||
<path d="M25.762 31.4644L24.1883 10.7838H32.5449L30.9932 31.4644H25.762ZM28.422 45.4459C27.8013 45.4459 27.2102 45.3215 26.6487 45.0725C26.1019 44.8391 25.6217 44.5045 25.2079 44.0688C24.7941 43.6331 24.469 43.1274 24.2326 42.5516C23.9961 41.9603 23.8779 41.3223 23.8779 40.6376C23.8779 39.9218 24.0035 39.2682 24.2548 38.6769C24.5207 38.07 24.868 37.5487 25.2966 37.113C25.7251 36.6773 26.2201 36.335 26.7817 36.086C27.358 35.837 27.9565 35.7125 28.5771 35.7125C29.1978 35.7125 29.7815 35.837 30.3283 36.086C30.8898 36.335 31.3774 36.6773 31.7912 37.113C32.205 37.5487 32.5301 38.0622 32.7665 38.6536C33.003 39.2449 33.1212 39.8829 33.1212 40.5676C33.1212 41.2678 32.9882 41.9214 32.7222 42.5283C32.471 43.1196 32.1237 43.6331 31.6804 44.0688C31.2518 44.5045 30.7568 44.8391 30.1953 45.0725C29.6337 45.3215 29.0426 45.4459 28.422 45.4459Z" fill="white" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({})
|
||||
</script>
|
|
@ -1,8 +1,11 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
import { Language } from 'vue3-gettext'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties extends Pick<Language, "$gettext" | "$pgettext" | "$ngettext" | "$npgettext"> {
|
||||
$language: Language;
|
||||
|
||||
lazyRenderMarkdown: (markdown: string) => Promise<string>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import './scss/main.scss'
|
||||
import { createApp } from 'vue'
|
||||
import VueMatomo from 'vue-matomo'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHistory, RouterScrollBehavior } from 'vue-router'
|
||||
import { createGettext } from 'vue3-gettext'
|
||||
import App from './App.vue'
|
||||
import Home from './views/Home.vue'
|
||||
import Search from './views/Search.vue'
|
||||
import CommonMixins from './mixins/CommonMixins'
|
||||
import SafeHTML from './components/SafeHTML.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
@ -59,7 +62,7 @@ if (allLocales.includes(localePath)) {
|
|||
} else if (languageFromLocalStorage) {
|
||||
currentLanguage = languageFromLocalStorage
|
||||
} else {
|
||||
const navigatorLanguage = (window.navigator as any).userLanguage || window.navigator.language
|
||||
const navigatorLanguage = window.navigator.language
|
||||
const snakeCaseLanguage = navigatorLanguage.replace('-', '_')
|
||||
currentLanguage = aliasesLanguages[snakeCaseLanguage] ? aliasesLanguages[snakeCaseLanguage] : snakeCaseLanguage
|
||||
}
|
||||
|
@ -79,6 +82,8 @@ buildTranslationsPromise(defaultLanguage, currentLanguage)
|
|||
})
|
||||
|
||||
app.use(gettext)
|
||||
app.mixin(CommonMixins)
|
||||
app.component('SafeHTML', SafeHTML)
|
||||
|
||||
// Routes
|
||||
let routes = []
|
||||
|
@ -91,7 +96,7 @@ buildTranslationsPromise(defaultLanguage, currentLanguage)
|
|||
routes = routes.concat([
|
||||
{
|
||||
path: base,
|
||||
component: Search
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: base + 'search',
|
||||
|
@ -108,6 +113,7 @@ buildTranslationsPromise(defaultLanguage, currentLanguage)
|
|||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
scrollBehavior: buildScrollBehaviour(),
|
||||
routes
|
||||
})
|
||||
|
||||
|
@ -150,7 +156,7 @@ function buildTranslationsPromise (defaultLanguage, currentLanguage) {
|
|||
if (currentLanguage === defaultLanguage) return Promise.resolve(translations)
|
||||
|
||||
// Fetch translations from server
|
||||
const fromRemote = import('./translations/' + currentLanguage + '.json')
|
||||
const fromRemote = import(`./translations/${currentLanguage}.json`)
|
||||
.then(module => {
|
||||
const remoteTranslations = module.default
|
||||
try {
|
||||
|
@ -176,3 +182,22 @@ function buildTranslationsPromise (defaultLanguage, currentLanguage) {
|
|||
|
||||
return fromRemote
|
||||
}
|
||||
|
||||
function buildScrollBehaviour (): RouterScrollBehavior {
|
||||
const isBot = /bot|googlebot|crawler|spider|robot|crawling/i.test(navigator.userAgent)
|
||||
const isGoogleCache = window.location.host === 'webcache.googleusercontent.com'
|
||||
|
||||
if (isBot || isGoogleCache) return undefined
|
||||
|
||||
return (to, from, savedPosition) => {
|
||||
if (savedPosition) return savedPosition
|
||||
if (to.hash) return { el: to.hash }
|
||||
if (to.path === from.path) return undefined
|
||||
|
||||
// Have to use a promise here
|
||||
// FIXME: https://github.com/vuejs/router/issues/1411
|
||||
return new Promise(res => setTimeout(() => {
|
||||
res({ top: 0, left: 0 })
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export default {
|
||||
methods: {
|
||||
async lazyRenderMarkdown (markdown: string) {
|
||||
const { renderMarkdown } = await import('../shared/markdown-render')
|
||||
|
||||
return renderMarkdown(markdown)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,15 +4,15 @@ export interface SearchUrl {
|
|||
host?: string
|
||||
publishedDateRange?: string
|
||||
durationRange?: string
|
||||
categoryOneOf?: number[]
|
||||
licenceOneOf?: number[]
|
||||
categoryOneOf?: string[]
|
||||
licenceOneOf?: string[]
|
||||
languageOneOf?: string[]
|
||||
|
||||
tagsAllOf?: string[]
|
||||
tagsOneOf?: string[]
|
||||
|
||||
isLive?: boolean | string
|
||||
isLive?: string
|
||||
|
||||
sort?: string
|
||||
page?: number | string
|
||||
page?: string
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
@import './_bootstrap-variables';
|
||||
|
||||
@import 'bootstrap/scss/functions';
|
||||
@import 'bootstrap/scss/variables';
|
||||
@import 'bootstrap/scss/maps';
|
||||
@import 'bootstrap/scss/mixins';
|
||||
@import 'bootstrap/scss/utilities';
|
|
@ -0,0 +1,24 @@
|
|||
@import "_variables";
|
||||
|
||||
$primary: $orange-main;
|
||||
$body-color: $main-font-color;
|
||||
|
||||
$link-color: $main-font-color;
|
||||
$link-decoration: none;
|
||||
|
||||
$container-max-widths: (
|
||||
sm: 540px,
|
||||
md: 720px,
|
||||
lg: 960px,
|
||||
xl: 1140px,
|
||||
xxl: 1270px
|
||||
);
|
||||
|
||||
$grid-breakpoints: (
|
||||
xs: 0,
|
||||
sm: 576px,
|
||||
md: 768px,
|
||||
lg: 992px,
|
||||
xl: 1200px,
|
||||
xxl: 1400px
|
||||
);
|
|
@ -0,0 +1,8 @@
|
|||
@import './_variables';
|
||||
|
||||
@import "bootstrap/scss/mixins";
|
||||
|
||||
@mixin all-width-block {
|
||||
margin-left: -$main-horizontal-padding;
|
||||
margin-right: -$main-horizontal-padding;
|
||||
}
|
|
@ -1,23 +1,32 @@
|
|||
$font-bold: 700;
|
||||
$font-semibold: 600;
|
||||
|
||||
$orange: #f67e08;
|
||||
$orange-darken: #f1680d;
|
||||
$orange-lighten: #fff8f0;
|
||||
$grey: #858383;
|
||||
$brown: #5e4f3a;
|
||||
$main-font-color: #212529;
|
||||
$secondary-font-color: #717679;
|
||||
|
||||
$orange-main: #f1680d;
|
||||
$orange-600: #D24C00;
|
||||
|
||||
$beige-300: #FFFAF6;
|
||||
$beige-400: #FFF5EB;
|
||||
$beige-500: #FCE8DB;
|
||||
$beige-600: #FCE1CF;
|
||||
$beige-700: #FFC99E;
|
||||
$beige-800: #FFB370;
|
||||
|
||||
$grey: #666;
|
||||
|
||||
$responsive-screen: 992px;
|
||||
$small-screen: 700px;
|
||||
|
||||
$primary: $orange;
|
||||
$secondary: $grey;
|
||||
|
||||
$thumbnail-width: 280px;
|
||||
$thumbnail-height: 153px;
|
||||
|
||||
$small-font-size: 12px;
|
||||
|
||||
$container-width: 1024px;
|
||||
$small-view: 900px;
|
||||
|
||||
$input-height: 30px;
|
||||
|
||||
$border-input-color: #C6C6C6;
|
||||
|
||||
$main-horizontal-padding: 1rem;
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
@import "_bootstrap-variables";
|
||||
|
||||
// Configuration
|
||||
@import "bootstrap/scss/functions";
|
||||
@import "bootstrap/scss/variables";
|
||||
@import "bootstrap/scss/maps";
|
||||
@import "bootstrap/scss/mixins";
|
||||
@import "bootstrap/scss/utilities";
|
||||
|
||||
// Layout & components
|
||||
@import "bootstrap/scss/root";
|
||||
@import "bootstrap/scss/reboot";
|
||||
@import "bootstrap/scss/type";
|
||||
// @import "bootstrap/scss/images";
|
||||
// @import "bootstrap/scss/containers";
|
||||
// @import "bootstrap/scss/grid";
|
||||
// @import "bootstrap/scss/tables";
|
||||
// @import "bootstrap/scss/forms";
|
||||
// @import "bootstrap/scss/buttons";
|
||||
// @import "bootstrap/scss/transitions";
|
||||
// @import "bootstrap/scss/dropdown";
|
||||
// @import "bootstrap/scss/button-group";
|
||||
// @import "bootstrap/scss/nav";
|
||||
// @import "bootstrap/scss/navbar";
|
||||
// @import "bootstrap/scss/card";
|
||||
// @import "bootstrap/scss/accordion";
|
||||
// @import "bootstrap/scss/breadcrumb";
|
||||
// @import "bootstrap/scss/pagination";
|
||||
// @import "bootstrap/scss/badge";
|
||||
// @import "bootstrap/scss/alert";
|
||||
// @import "bootstrap/scss/progress";
|
||||
// @import "bootstrap/scss/list-group";
|
||||
// @import "bootstrap/scss/close";
|
||||
// @import "bootstrap/scss/toasts";
|
||||
// @import "bootstrap/scss/modal";
|
||||
// @import "bootstrap/scss/tooltip";
|
||||
@import "bootstrap/scss/popover";
|
||||
// @import "bootstrap/scss/carousel";
|
||||
// @import "bootstrap/scss/spinners";
|
||||
// @import "bootstrap/scss/offcanvas";
|
||||
// @import "bootstrap/scss/placeholders";
|
||||
|
||||
// Helpers
|
||||
@import "bootstrap/scss/helpers";
|
||||
|
||||
// Utilities
|
||||
@import "bootstrap/scss/utilities/api";
|
||||
|
||||
.fs-7 {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
@use 'sass:color';
|
||||
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import '_bootstrap-mixins';
|
||||
|
||||
.peertube-radio-container {
|
||||
[type=radio]:checked,
|
||||
[type=radio]:not(:checked) {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
[type=radio]:checked + label,
|
||||
[type=radio]:not(:checked) + label {
|
||||
position: relative;
|
||||
padding-left: 28px;
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
display: inline-block;
|
||||
font-weight: normal
|
||||
}
|
||||
|
||||
[type=radio]:checked + label::before,
|
||||
[type=radio]:not(:checked) + label::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1px solid #C6C6C6;
|
||||
border-radius: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
[type=radio]:checked + label::after,
|
||||
[type=radio]:not(:checked) + label::after {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: $orange-main;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
border-radius: 100%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
[type=radio]:not(:checked) + label::after {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
[type=radio]:checked + label::after {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.peertube-button {
|
||||
padding: 0.5rem 1rem;
|
||||
display: inline-block;
|
||||
font-weight: $font-bold;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
border: 2px solid $orange-main;
|
||||
transition: opacity 0.2s;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
line-height: 1.35;
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 0.25rem $main-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.peertube-primary-button {
|
||||
border: 2px solid $orange-main;
|
||||
color: #fff;
|
||||
background: $orange-main;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8 !important;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.peertube-secondary-button {
|
||||
color: $main-font-color;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
background: rgb(242, 105, 13, 0.1);
|
||||
color: $main-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.peertube-button-link {
|
||||
&, &:hover, &:focus, &:active {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.select-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
|
||||
&:after {
|
||||
top: 50%;
|
||||
right: calc(0% + 15px);
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 5px solid rgba(0, 0, 0, 0);
|
||||
border-top-color: #000;
|
||||
margin-top: -2px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0 35px 0 12px;
|
||||
position: relative;
|
||||
border: 1px solid $border-input-color;
|
||||
background: transparent none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
height: $input-height;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:-moz-focusring {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 #000;
|
||||
}
|
||||
|
||||
option {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.classic-input-text {
|
||||
display: block;
|
||||
min-height: 30px;
|
||||
width: 100%;
|
||||
border: 1px solid $border-input-color;
|
||||
padding: 0 35px 0 12px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.ti-tag {
|
||||
background: $orange-600 !important;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.peertube-link {
|
||||
color: $main-font-color;
|
||||
font-weight: $font-bold;
|
||||
border-bottom: 3px solid $orange-main;
|
||||
transition: border-bottom-width 0.2s ease;
|
||||
text-decoration: none;
|
||||
transition: color 0.1s;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: color.scale($main-font-color, $lightness: +20%);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.card {
|
||||
@include padding(2.5rem);
|
||||
|
||||
background-color: $beige-500;
|
||||
border: 1px solid $beige-700;
|
||||
border-radius: 16px;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
@include all-width-block;
|
||||
@include padding(2.5rem 1rem);
|
||||
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-card-result {
|
||||
@include padding(1.5rem);
|
||||
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
border: 1px solid $beige-700;
|
||||
border-radius: 4px;
|
||||
|
||||
h4 {
|
||||
color: $main-font-color;
|
||||
font-size: 1.25rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
& + .small-separator {
|
||||
margin-bottom: 1rem;
|
||||
height: 3px;
|
||||
width: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-container {
|
||||
margin-right: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metadata,
|
||||
.description {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
label {
|
||||
color: $secondary-font-color;
|
||||
margin-top: 0.25rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button-link-container {
|
||||
text-align: right;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
@include all-width-block;
|
||||
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
border-radius: 0;
|
||||
flex-direction: column;
|
||||
|
||||
.image-container {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.button-link-container {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.muted {
|
||||
color: $grey;
|
||||
}
|
||||
|
||||
.wrap-text {
|
||||
word-break: break-word;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.small-separator {
|
||||
width: 50px;
|
||||
height: 6px;
|
||||
background: #f2690d;
|
||||
border-radius: 4px;
|
||||
}
|
|
@ -1,244 +1,63 @@
|
|||
@import "_variables";
|
||||
@import "./progress";
|
||||
@use 'sass:color';
|
||||
|
||||
$border-input-color: #C6C6C6;
|
||||
@import '_variables';
|
||||
|
||||
@import 'bootstrap.scss';
|
||||
|
||||
@import './progress';
|
||||
@import './classes';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
|
||||
font-size: 16px;
|
||||
background-color: #ffad5c;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
|
||||
@media screen and (max-width: $container-width) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
width: $container-width;
|
||||
background-color: #fff;
|
||||
margin: auto;
|
||||
padding: 50px 80px;
|
||||
position: relative;
|
||||
|
||||
@media screen and (max-width: $container-width) {
|
||||
width: 100%;
|
||||
padding: 50px 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-screen) {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.button-link,
|
||||
.peertube-button {
|
||||
background-color: $orange-darken;
|
||||
color: #fff;
|
||||
font-weight: $font-semibold;
|
||||
border-radius: 5px;
|
||||
padding: 7px 20px;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
.peertube-button {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 17px 0 13px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.none-opacity {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
|
||||
&:after {
|
||||
top: 50%;
|
||||
right: calc(0% + 15px);
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 5px solid rgba(0, 0, 0, 0);
|
||||
border-top-color: #000;
|
||||
margin-top: -2px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0 35px 0 12px;
|
||||
position: relative;
|
||||
border: 1px solid $border-input-color;
|
||||
background: transparent none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
height: $input-height;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:-moz-focusring {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 #000;
|
||||
}
|
||||
|
||||
option {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.classic-input-text {
|
||||
display: block;
|
||||
min-height: 30px;
|
||||
width: 100%;
|
||||
border: 1px solid $border-input-color;
|
||||
padding: 0 35px 0 12px;
|
||||
*:focus-visible {
|
||||
box-shadow: 0 0 0 0.25rem $orange-main;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.results {
|
||||
.root-result {
|
||||
display: flex;
|
||||
margin: 30px 0;
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
|
||||
line-height: 1.58;
|
||||
background-color: #FFFAF6;
|
||||
color: $main-font-color;
|
||||
}
|
||||
|
||||
.information {
|
||||
width: 100%;
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.title-block {
|
||||
.container {
|
||||
@include padding(4rem $main-horizontal-padding);
|
||||
|
||||
> *{
|
||||
display: inline-block;
|
||||
max-width: $container-width;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.handle {
|
||||
font-size: $small-font-size;
|
||||
color: $grey;
|
||||
input[type=text],
|
||||
.vue-tags-input {
|
||||
font-size: 0.875rem;
|
||||
|
||||
&.ti-focus,
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 .25rem $beige-600 !important;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 .15rem $orange-main !important;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0 5px 0 0;
|
||||
word-break: break-word;
|
||||
.vue-tags-input {
|
||||
max-width: unset !important;
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
input[type=text] {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.additional-information {
|
||||
margin: 5px 0 20px 0;
|
||||
font-size: $small-font-size;
|
||||
color: $grey;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 10px 0 20px 0;
|
||||
border-left: 1px solid #dee2e6;
|
||||
padding-left: 10px;
|
||||
word-break: break-word;
|
||||
|
||||
a {
|
||||
color: #0056b3;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metadata {
|
||||
> div {
|
||||
min-height: 27px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: $small-font-size;
|
||||
}
|
||||
|
||||
label {
|
||||
color: $grey;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.button-link {
|
||||
display: inline-block;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
.root-result {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin: 0 auto 20px auto !important;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
margin: 0 0 20px -10px !important;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
label {
|
||||
min-width: 70px;
|
||||
margin-right: 10px;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ti-tag {
|
||||
background: $orange-darken !important;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: $orange-darken;
|
||||
background: $orange-600;
|
||||
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
|
@ -24,7 +24,7 @@
|
|||
right: 0px;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 10px $orange-darken, 0 0 5px $orange-darken;
|
||||
box-shadow: 0 0 10px $orange-600, 0 0 5px $orange-600;
|
||||
opacity: 1.0;
|
||||
|
||||
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||
|
@ -38,7 +38,7 @@
|
|||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
|
@ -47,8 +47,8 @@
|
|||
box-sizing: border-box;
|
||||
|
||||
border: solid 2px transparent;
|
||||
border-top-color: $orange-darken;
|
||||
border-left-color: $orange-darken;
|
||||
border-top-color: $orange-600;
|
||||
border-left-color: $orange-600;
|
||||
border-radius: 50%;
|
||||
|
||||
-webkit-animation: nprogress-spinner 400ms linear infinite;
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
function parseHTML (html: string) {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = html
|
||||
|
||||
return div as HTMLDivElement
|
||||
}
|
||||
|
||||
function getChildNodes (node: HTMLElement) {
|
||||
return node.childNodes as NodeListOf<HTMLElement>
|
||||
}
|
||||
|
||||
function getAttribute (node: HTMLElement, key: string) {
|
||||
return node.getAttribute(key)
|
||||
}
|
||||
|
||||
function getClass (node: HTMLElement) {
|
||||
return node.className
|
||||
}
|
||||
|
||||
function getText (node: HTMLElement) {
|
||||
return node.textContent
|
||||
}
|
||||
|
||||
function nodeIs (node: HTMLElement, tagName: string) {
|
||||
if (tagName === 'TEXT') {
|
||||
return node.nodeType === 3
|
||||
}
|
||||
|
||||
return node.tagName === tagName
|
||||
}
|
||||
|
||||
export {
|
||||
parseHTML,
|
||||
|
||||
getChildNodes,
|
||||
getClass,
|
||||
getAttribute,
|
||||
getText,
|
||||
nodeIs
|
||||
}
|
|
@ -11,36 +11,42 @@ const baseVideosPath = '/api/v1/search/videos'
|
|||
const baseVideoChannelsPath = '/api/v1/search/video-channels'
|
||||
const baseVideoPlaylistsPath = '/api/v1/search/video-playlists'
|
||||
|
||||
function searchVideos (params: VideosSearchQuery) {
|
||||
const options = {
|
||||
params
|
||||
function searchVideos (options: VideosSearchQuery) {
|
||||
const axiosOptions = {
|
||||
params: {
|
||||
...options,
|
||||
|
||||
search: options.search || undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (params.search) Object.assign(options.params, { search: params.search })
|
||||
|
||||
return axios.get<ResultList<EnhancedVideo>>(buildApiUrl(baseVideosPath), options)
|
||||
return axios.get<ResultList<EnhancedVideo>>(buildApiUrl(baseVideosPath), axiosOptions)
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
function searchVideoChannels (params: VideoChannelsSearchQuery) {
|
||||
const options = {
|
||||
params
|
||||
function searchVideoChannels (options: VideoChannelsSearchQuery) {
|
||||
const axiosOptions = {
|
||||
params: {
|
||||
...options,
|
||||
|
||||
search: options.search || undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (params.search) Object.assign(options.params, { search: params.search })
|
||||
|
||||
return axios.get<ResultList<EnhancedVideoChannel>>(buildApiUrl(baseVideoChannelsPath), options)
|
||||
return axios.get<ResultList<EnhancedVideoChannel>>(buildApiUrl(baseVideoChannelsPath), axiosOptions)
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
function searchVideoPlaylists (params: VideoPlaylistsSearchQuery) {
|
||||
const options = {
|
||||
params
|
||||
function searchVideoPlaylists (options: VideoPlaylistsSearchQuery) {
|
||||
const axiosOptions = {
|
||||
params: {
|
||||
...options,
|
||||
|
||||
search: options.search || undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (params.search) Object.assign(options.params, { search: params.search })
|
||||
|
||||
return axios.get<ResultList<EnhancedPlaylist>>(buildApiUrl(baseVideoPlaylistsPath), options)
|
||||
return axios.get<ResultList<EnhancedPlaylist>>(buildApiUrl(baseVideoPlaylistsPath), axiosOptions)
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
|
|
|
@ -51,9 +51,9 @@ function durationRangeToAPIParams (durationRange: string) {
|
|||
}
|
||||
|
||||
function publishedDateRangeToAPIParams (publishedDateRange: string) {
|
||||
if (!publishedDateRange) {
|
||||
return { startDate: undefined, endDate: undefined }
|
||||
}
|
||||
const empty = { startDate: undefined, endDate: undefined }
|
||||
|
||||
if (!publishedDateRange) return empty
|
||||
|
||||
// today
|
||||
const date = new Date()
|
||||
|
@ -75,9 +75,12 @@ function publishedDateRangeToAPIParams (publishedDateRange: string) {
|
|||
date.setDate(date.getDate() - 365)
|
||||
break
|
||||
|
||||
case 'any_published_date':
|
||||
return empty
|
||||
|
||||
default:
|
||||
console.error('Unknown published date range %s', publishedDateRange)
|
||||
return { startDate: undefined, endDate: undefined }
|
||||
return empty
|
||||
}
|
||||
|
||||
return { startDate: date.toISOString(), endDate: undefined }
|
||||
|
@ -93,12 +96,45 @@ function extractTagsFromQuery <T> (value: T | T[]) {
|
|||
return [ { text: value } ]
|
||||
}
|
||||
|
||||
function extractQueryToStringArray (value: string | string[]) {
|
||||
if (!value) return undefined
|
||||
|
||||
if (Array.isArray(value)) return value
|
||||
|
||||
return [ value ]
|
||||
}
|
||||
|
||||
function extractQueryToIntArray (value: string | string[]) {
|
||||
if (!value) return undefined
|
||||
|
||||
if (Array.isArray(value)) return value.map(v => parseInt(v))
|
||||
|
||||
return [ parseInt(value) ]
|
||||
}
|
||||
|
||||
function extractQueryToInt (value: string) {
|
||||
if (!value) return undefined
|
||||
|
||||
return parseInt(value)
|
||||
}
|
||||
|
||||
function extractQueryToBoolean (value: string) {
|
||||
if (value === 'true') return true
|
||||
if (value === 'false') return false
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export {
|
||||
buildApiUrl,
|
||||
durationToString,
|
||||
publishedDateRangeToAPIParams,
|
||||
pageToAPIParams,
|
||||
durationRangeToAPIParams,
|
||||
extractTagsFromQuery
|
||||
extractTagsFromQuery,
|
||||
extractQueryToStringArray,
|
||||
extractQueryToIntArray,
|
||||
extractQueryToInt,
|
||||
extractQueryToBoolean
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<div>
|
||||
<my-header :index-name="indexName" :small-format="false" />
|
||||
|
||||
<main>
|
||||
<div class="card search-home">
|
||||
<img :src="searchImageUrl" alt="" />
|
||||
|
||||
<form id="search-anchor" role="search" onsubmit="return false;">
|
||||
<search-input :label="false"></search-input>
|
||||
</form>
|
||||
|
||||
<h3>
|
||||
<SafeHTML>
|
||||
{{
|
||||
$gettext(
|
||||
'Search for your favorite videos, channels and playlists on <a class="peertube-link" href="%{indexedInstancesUrl}" target="_blank">%{instancesCount} PeerTube websites</a> indexed by %{indexName}!',
|
||||
{ instancesCount: instancesCount, indexedInstancesUrl: indexedInstancesUrl, indexName: indexName },
|
||||
true
|
||||
)
|
||||
}}
|
||||
</SafeHTML>
|
||||
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<search-warning class="search-warning" :index-name="indexName" :is-block-mode="true" />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import Header from '../components/Header.vue'
|
||||
import SearchWarning from '../components/SearchWarning.vue'
|
||||
import SearchInput from '../components/SearchInput.vue'
|
||||
import { getConfig } from '../shared/config'
|
||||
import { buildApiUrl } from '@/shared/utils'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
'search-input': SearchInput,
|
||||
'search-warning': SearchWarning,
|
||||
'my-header': Header
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
instancesCount: undefined,
|
||||
indexedInstancesUrl: '',
|
||||
indexName: '',
|
||||
formSearch: '',
|
||||
searchImageUrl: '',
|
||||
|
||||
introSentence: ''
|
||||
}
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
const config = await getConfig()
|
||||
|
||||
this.instancesCount = config.indexedHostsCount
|
||||
this.indexedInstancesUrl = config.indexedInstancesUrl
|
||||
this.indexName = config.searchInstanceName
|
||||
|
||||
this.searchImageUrl = config.searchInstanceSearchImage
|
||||
? buildApiUrl(config.searchInstanceSearchImage)
|
||||
: buildApiUrl('/img/sepia-search.svg')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use 'sass:math';
|
||||
|
||||
@import '../scss/_variables';
|
||||
@import '../scss/bootstrap-mixins';
|
||||
|
||||
main {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include margin(2rem auto 0 auto);
|
||||
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
font-size: 1rem;
|
||||
line-height: inherit;
|
||||
|
||||
a {
|
||||
color: $orange-main;
|
||||
|
||||
&:hover {
|
||||
color: $orange-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
@include margin-bottom(4rem);
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 290px;
|
||||
height: 250px;
|
||||
margin: 0 auto 1rem auto;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 700px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.search-home img {
|
||||
position: relative;
|
||||
bottom: -8px;
|
||||
margin-bottom: 0;
|
||||
z-index: 100;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
bottom: -5px;
|
||||
}
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
|
@ -10,6 +10,8 @@
|
|||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"paths": {
|
||||
"@shared/*": [ "../PeerTube/shared/*" ],
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
visualizer({ open: true })
|
||||
],
|
||||
server: {
|
||||
port: 8080
|
||||
},
|
||||
resolve:{
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
2013
client/yarn.lock
2013
client/yarn.lock
File diff suppressed because it is too large
Load Diff
|
@ -6,7 +6,6 @@ elastic-search:
|
|||
|
||||
search-instance:
|
||||
name_image: '/theme/framasoft/img/title.svg'
|
||||
search_image: '/theme/framasoft/img/sepia-search.svg'
|
||||
theme: 'framasoft'
|
||||
|
||||
videos-search:
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
"eslint": "^8.16.0",
|
||||
"eslint-config-standard-with-typescript": "^24.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.6.0",
|
||||
"eslint-plugin-node": "^11.0.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
header {
|
||||
font-family: inherit !important;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
header h4 {
|
||||
font-size: 16px;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
header h4 a {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
header .search-home {
|
||||
margin: 50px 0 -7px 0 !important;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
header .search-home {
|
||||
margin-top: 30px !important;
|
||||
margin-bottom: -4px !important;
|
||||
width: 150px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-height: 400px) {
|
||||
.search-container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
35
yarn.lock
35
yarn.lock
|
@ -553,6 +553,13 @@ buffer@^6.0.3:
|
|||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
builtins@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9"
|
||||
integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==
|
||||
dependencies:
|
||||
semver "^7.0.0"
|
||||
|
||||
busboy@^1.0.0, busboy@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
||||
|
@ -886,6 +893,14 @@ eslint-plugin-es@^3.0.0:
|
|||
eslint-utils "^2.0.0"
|
||||
regexpp "^3.0.0"
|
||||
|
||||
eslint-plugin-es@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz#f0822f0c18a535a97c3e714e89f88586a7641ec9"
|
||||
integrity sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==
|
||||
dependencies:
|
||||
eslint-utils "^2.0.0"
|
||||
regexpp "^3.0.0"
|
||||
|
||||
eslint-plugin-import@^2.26.0:
|
||||
version "2.26.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b"
|
||||
|
@ -905,6 +920,20 @@ eslint-plugin-import@^2.26.0:
|
|||
resolve "^1.22.0"
|
||||
tsconfig-paths "^3.14.1"
|
||||
|
||||
eslint-plugin-n@^15.6.0:
|
||||
version "15.6.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-15.6.0.tgz#cfb1d2e2e427d620eb9008f8b3b5a40de0c84120"
|
||||
integrity sha512-Hd/F7wz4Mj44Jp0H6Jtty13NcE69GNTY0rVlgTIj1XBnGGVI6UTdDrpE6vqu3AHo07bygq/N+7OH/lgz1emUJw==
|
||||
dependencies:
|
||||
builtins "^5.0.1"
|
||||
eslint-plugin-es "^4.1.0"
|
||||
eslint-utils "^3.0.0"
|
||||
ignore "^5.1.1"
|
||||
is-core-module "^2.11.0"
|
||||
minimatch "^3.1.2"
|
||||
resolve "^1.22.1"
|
||||
semver "^7.3.8"
|
||||
|
||||
eslint-plugin-node@^11.0.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
|
||||
|
@ -1549,7 +1578,7 @@ is-callable@^1.1.4, is-callable@^1.2.7:
|
|||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
|
||||
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
|
||||
|
||||
is-core-module@^2.8.1, is-core-module@^2.9.0:
|
||||
is-core-module@^2.11.0, is-core-module@^2.8.1, is-core-module@^2.9.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
|
||||
integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
|
||||
|
@ -2334,7 +2363,7 @@ resolve-from@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
resolve@^1.10.1, resolve@^1.20.0, resolve@^1.22.0:
|
||||
resolve@^1.10.1, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1:
|
||||
version "1.22.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
|
||||
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
|
||||
|
@ -2406,7 +2435,7 @@ semver@^6.1.0:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.3.7:
|
||||
semver@^7.0.0, semver@^7.3.7, semver@^7.3.8:
|
||||
version "7.3.8"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
|
||||
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
|
||||
|
|
Loading…
Reference in New Issue