Migrate to vite

This commit is contained in:
Chocobozzz 2022-02-23 15:21:02 +01:00
parent b2e255d97b
commit 468007dba8
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
30 changed files with 2646 additions and 6231 deletions

View File

@ -20,7 +20,7 @@ $ node dist/server
```
```
$ cd client && npm run serve
$ cd client && npm run dev
```
Then open http://localhost:8080.

View File

@ -1,2 +1,2 @@
VUE_APP_API_URL=http://localhost:3234
#VUE_APP_API_URL=https://search.joinpeertube.org
VITE_APP_API_URL=http://localhost:3234
#VITE_APP_API_URL=https://search.joinpeertube.org

18
client/.eslintrc.json Normal file
View File

@ -0,0 +1,18 @@
{
"root": true,
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser"
},
"plugins": [ "@typescript-eslint" ],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended"
],
"rules": {
"vue/multi-word-component-names": "off",
"vue/require-default-prop": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}

1
client/.gitignore vendored
View File

@ -1 +1,2 @@
src/locale/**/*~
dist/

View File

@ -1,5 +0,0 @@
{
"plugins": {
"autoprefixer": {}
}
}

View File

@ -6,7 +6,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" type="image/png" href="<%= BASE_URL %>img/favicon.png">
<link rel="icon" type="image/png" href="/img/favicon.png">
</head>
<body>
@ -15,9 +15,7 @@
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<div id="footer"></div>
<!-- built files will be auto injected -->
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -3,34 +3,32 @@
"version": "0.0.1",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --mode development",
"build": "vue-cli-service build --mode production",
"lint": "vue-cli-service lint",
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore --fix src",
"i18n:update": "git fetch weblate && git merge weblate/master && rm -f src/locale/en_US/LC_MESSAGES/app.po && make clean && make makemessages && make translations"
},
"dependencies": {},
"devDependencies": {
"@jshmrtn/vue3-gettext": "^1.5.0",
"@sipec/vue3-tags-input": "^3.0.4",
"@types/axios": "^0.14.0",
"@types/markdown-it": "^10.0.1",
"@vue/cli-plugin-typescript": "~5.0.0-beta.2",
"@vue/cli-service": "~5.0.0-beta.2",
"@vue/compiler-sfc": "^3.1.0",
"axios": "^0.20.0",
"markdown-it": "^11.0.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",
"eslint": "^8.9.0",
"eslint-plugin-vue": "^8.5.0",
"markdown-it": "^12.3.2",
"nprogress": "^0.2.0",
"register-service-worker": "^1.0.0",
"sass": "^1.35.1",
"sass-loader": "^12.1.0",
"typescript": "~4.3.4",
"vue": "^3.1.0",
"vue-matomo": "^4.0.1",
"vue-router": "^4.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
"sass": "^1.49.8",
"typescript": "~4.5.5",
"vite": "^2.8.4",
"vue": "^3.2.31",
"vue-matomo": "^4.1.0",
"vue-router": "^4.0.12",
"vue-tsc": "^0.31.4",
"vue3-gettext": "^2.1.0"
}
}

View File

@ -1,8 +1,11 @@
<template>
<div>
<router-view id="main-container" class="container"/>
<router-view
id="main-container"
class="container"
/>
<my-footer></my-footer>
<my-footer />
</div>
</template>
@ -18,7 +21,7 @@
data () {
return {
title: process.env.VUE_APP_TITLE
title: import.meta.env.VITE_APP_TITLE
}
}
})

View File

@ -1,15 +1,63 @@
<template>
<a v-bind:href="actor.url" rel="nofollow noreferrer noopener" target="_blank" class="actor" v-bind:title="linkTitle">
<img v-if="actor.avatar && !avatarError" v-bind:src="actor.avatar.url" alt="" :class="{ account: isAccount }" @error="setAvatarError()">
<a
:href="actor.url"
rel="nofollow noreferrer noopener"
target="_blank"
class="actor"
:title="linkTitle"
>
<img
v-if="actor.avatar && !avatarError"
:src="actor.avatar.url"
alt=""
:class="{ account: isAccount }"
@error="setAvatarError()"
>
<strong>{{ actor.displayName }}</strong>
<span class="actor-handle">
{{ actor.name }}@{{actor.host}}
{{ actor.name }}@{{ actor.host }}
</span>
</a>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { AccountSummary, VideoChannelSummary } from '../../../PeerTube/shared/models'
export default defineComponent({
props: {
actor: Object as PropType<AccountSummary | VideoChannelSummary>,
type: String
},
data () {
return {
avatarError: false
}
},
computed: {
linkTitle (): string {
if (this.type === 'channel') return this.$gettext('Go on this channel page')
return this.$gettext('Go on this account page')
},
isAccount (): boolean {
return this.type === 'account'
}
},
methods: {
setAvatarError () {
this.avatarError = true
}
}
})
</script>
<style lang="scss">
@import '../scss/_variables';
@ -56,39 +104,3 @@
}
</style>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { AccountSummary, VideoChannelSummary } from '../../../PeerTube/shared/models'
export default defineComponent({
props: {
actor: Object as PropType<AccountSummary | VideoChannelSummary>,
type: String
},
data () {
return {
avatarError: false
}
},
methods: {
setAvatarError () {
this.avatarError = true
}
},
computed: {
linkTitle (): string {
if (this.type === 'channel') return this.$gettext('Go on this channel page')
return this.$gettext('Go on this account page')
},
isAccount (): boolean {
return this.type === 'account'
}
}
})
</script>

View File

@ -1,15 +1,33 @@
<template>
<div class="channel root-result">
<a target="_blank" rel="nofollow noreferrer noopener" v-bind:href="channel.url" :title="discoverChannelMessage" class="avatar">
<img v-if="channel.avatar" v-bind:src="channel.avatar.url" alt="">
<img v-else src="/img/default-avatar.png" alt="">
<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=""
>
</a>
<div class="information">
<div class="title-block">
<h5 class="title">
<a target="_blank" rel="nofollow noreferrer noopener" v-bind:href="channel.url" :title="discoverChannelMessage">
<a
target="_blank"
rel="nofollow noreferrer noopener"
:href="channel.url"
:title="discoverChannelMessage"
>
{{ channel.displayName }}
</a>
</h5>
@ -23,40 +41,24 @@
</div>
</div>
<div class="description">{{ channel.description }}</div>
<div class="description">
{{ channel.description }}
</div>
<div class="button">
<a class="button-link" rel="nofollow noreferrer noopener" target="_blank" v-bind:href="channel.url">
<a
class="button-link"
rel="nofollow noreferrer noopener"
target="_blank"
:href="channel.url"
>
{{ discoverChannelMessage }}
</a>
</div>
</div>
</div>
</template>
<style lang="scss">
@import '../scss/_variables';
.channel {
.avatar {
width: $thumbnail-width;
min-width: $thumbnail-width;
margin-right: 20px;
display: flex;
justify-content: center;
img {
object-fit: cover;
width: 110px;
height: 110px;
min-width: 110px;
min-height: 110px;
}
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { VideoChannel } from '../../../PeerTube/shared/models'
@ -85,3 +87,25 @@
}
})
</script>
<style lang="scss">
@import '../scss/_variables';
.channel {
.avatar {
width: $thumbnail-width;
min-width: $thumbnail-width;
margin-right: 20px;
display: flex;
justify-content: center;
img {
object-fit: cover;
width: 110px;
height: 110px;
min-width: 110px;
min-height: 110px;
}
}
}
</style>

View File

@ -1,42 +1,107 @@
<template>
<footer id="main-footer">
<div class="header">
<img src="/img/bottom-peertube-logo-v3.svg" alt="">
<img
src="/img/bottom-peertube-logo-v3.svg"
alt=""
>
<div v-translate class="description">A free software to take back control of your videos</div>
<div
v-translate
class="description"
>
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
v-translate
class="subtitle"
>
Open your own videos website with PeerTube!
</div>
<a target="_blank" v-translate href="https://docs.joinpeertube.org/#/install-any-os">Install PeerTube</a>
<a
v-translate
target="_blank"
href="https://docs.joinpeertube.org/#/install-any-os"
>Install PeerTube</a>
<a target="_blank" v-translate href="https://joinpeertube.org#what-is-peertube">Why should I have my own PeerTube website?</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>
<div
v-translate
class="subtitle"
>
Create an account to take back control of your videos
</div>
<a target="_blank" v-translate href="https://joinpeertube.org/instances">Open an account on a PeerTube website</a>
<a
v-translate
target="_blank"
href="https://joinpeertube.org/instances"
>Open an account on a PeerTube website</a>
<a target="_blank" v-translate href="https://docs.joinpeertube.org/#/use-library?id=playlist">Create playlists</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 href="https://joinpeertube.org" target="_blank" v-translate> &gt;&gt; Check all guides on joinpeertube.org &lt;&lt; </a>
<a
v-translate
href="https://joinpeertube.org"
target="_blank"
> &gt;&gt; Check all guides on joinpeertube.org &lt;&lt; </a>
</div>
<div class="footer">
<a v-translate href="https://framagit.org/framasoft/peertube/search-index/" target="_blank">Source code</a>
<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"
v-translate
:href="legalNoticesUrl"
target="_blank"
>Legal notices</a>
</div>
</footer>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { getConfig } from '../shared/config'
export default defineComponent({
data () {
return {
legalNoticesUrl: ''
}
},
async mounted () {
const config = await getConfig()
this.legalNoticesUrl = config.legalNoticesUrl
}
})
</script>
<style lang="scss">
@import '../scss/_variables';
@ -152,22 +217,3 @@
}
}
</style>
<script>
import { defineComponent } from 'vue'
import { getConfig } from '../shared/config'
export default defineComponent({
data () {
return {
legalNoticesUrl: ''
}
},
async mounted () {
const config = await getConfig()
this.legalNoticesUrl = config.legalNoticesUrl
}
})
</script>

View File

@ -1,29 +1,87 @@
<template>
<div>
<header>
<interface-language-dropdown class="interface-language-dropdown"></interface-language-dropdown>
<interface-language-dropdown class="interface-language-dropdown" />
<h1>
<span v-if="configLoaded && !titleImageUrl">{{indexName}}</span>
<span v-if="configLoaded && !titleImageUrl">{{ indexName }}</span>
<img class="title-image" :src="titleImageUrl" :alt="indexName" />
<img
class="title-image"
:src="titleImageUrl"
:alt="indexName"
>
</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>
<div v-translate>
A search engine of <a
href="https://joinpeertube.org"
target="_blank"
>PeerTube</a> videos, channels and playlists
</div>
<div v-translate>Developed by <a href="https://framasoft.org" target="_blank">Framasoft</a></div>
<div v-translate>
Developed by <a
href="https://framasoft.org"
target="_blank"
>Framasoft</a>
</div>
</h4>
<div class="search-home">
<img v-if="searchImageUrl" :src="searchImageUrl" alt="">
<img
v-if="searchImageUrl"
:src="searchImageUrl"
alt=""
>
</div>
</template>
</header>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { getConfig } from '../shared/config'
import { buildApiUrl } from '../shared/utils'
import InterfaceLanguageDropdown from './InterfaceLanguageDropdown.vue'
export default defineComponent({
components: {
'interface-language-dropdown': InterfaceLanguageDropdown
},
props: {
indexName: String,
smallFormat: Boolean,
},
data () {
return {
configLoaded: false,
titleImageUrl: '',
searchImageUrl: ''
}
},
async mounted () {
const config = await getConfig()
this.titleImageUrl = config.searchInstanceNameImage
? buildApiUrl(config.searchInstanceNameImage)
: ''
this.searchImageUrl = config.searchInstanceSearchImage
? buildApiUrl(config.searchInstanceSearchImage)
: buildApiUrl('/img/search-home.png')
this.configLoaded = true
}
})
</script>
<style lang="scss">
@import '../scss/_variables';
@ -108,43 +166,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent } from 'vue'
import { getConfig } from '../shared/config'
import { buildApiUrl } from '../shared/utils'
import InterfaceLanguageDropdown from './InterfaceLanguageDropdown.vue'
export default defineComponent({
components: {
'interface-language-dropdown': InterfaceLanguageDropdown
},
data () {
return {
configLoaded: false,
titleImageUrl: '',
searchImageUrl: ''
}
},
props: {
indexName: String,
smallFormat: Boolean,
},
async mounted () {
const config = await getConfig()
this.titleImageUrl = config.searchInstanceNameImage
? buildApiUrl(config.searchInstanceNameImage)
: ''
this.searchImageUrl = config.searchInstanceSearchImage
? buildApiUrl(config.searchInstanceSearchImage)
: buildApiUrl('/img/search-home.png')
this.configLoaded = true
}
})
</script>

View File

@ -1,21 +1,80 @@
<template>
<div class="interface-language-dropdown">
<img :title="imgTitle" v-on:click="toggleShow()" class="interface-language" src="/img/interface-languages.svg" alt="Change interface language">
<img
:title="imgTitle"
class="interface-language"
src="/img/interface-languages.svg"
alt="Change interface language"
@click="toggleShow()"
>
<div v-if="showMenu" class="menu">
<a v-for="(lang, locale) in availableLanguages" :key="locale" :href="buildLanguageRoute(locale)" class="menu-item">
{{lang}}
<div
v-if="showMenu"
class="menu"
>
<a
v-for="(lang, locale) in availableLanguages"
:key="locale"
:href="buildLanguageRoute(locale)"
class="menu-item"
>
{{ lang }}
</a>
<hr />
<hr>
<a class="menu-item add-your-language" target="_blank" href="https://weblate.framasoft.org/projects/peertube-search-index/client/">
<a
class="menu-item add-your-language"
target="_blank"
href="https://weblate.framasoft.org/projects/peertube-search-index/client/"
>
Translate
</a>
</div>
</div>
</template>
<script lang="ts">
import { useGettext } from 'vue3-gettext'
import { defineComponent } from 'vue'
export default defineComponent({
data () {
return {
showMenu: false
}
},
computed: {
imgTitle () {
return this.$gettext('Change interface language')
},
availableLanguages (): { [id: string]: string } {
const { available } = useGettext()
return available
}
},
methods: {
toggleShow () {
this.showMenu = !this.showMenu;
},
buildLanguageRoute(locale: string | number) {
const paths = this.$route.fullPath.split('/')
if (paths.length > 0 && Object.prototype.hasOwnProperty.call(this.availableLanguages, paths[0])) {
return '/' + locale + '/' + paths.slice(1).join('/')
}
return '/' + locale + this.$route.fullPath
}
}
})
</script>
<style lang="scss">
@import '../scss/_variables';
@ -67,44 +126,3 @@
}
}
</style>
<script lang="ts">
import { useGettext } from '@jshmrtn/vue3-gettext'
import { defineComponent } from 'vue'
export default defineComponent({
data () {
return {
showMenu: false
}
},
computed: {
imgTitle () {
return this.$gettext('Change interface language')
},
availableLanguages (): { [id: string]: string } {
const { available } = useGettext()
return available
}
},
methods: {
toggleShow () {
this.showMenu = !this.showMenu;
},
buildLanguageRoute(locale: string) {
const paths = this.$route.fullPath.split('/')
if (paths.length > 0 && this.availableLanguages.hasOwnProperty(paths[0])) {
return "/" + locale + "/" + paths.slice(1).join("/")
} else {
return "/" + locale + this.$route.fullPath
}
}
}
})
</script>

View File

@ -1,29 +1,69 @@
<template>
<div class="pagination" v-if="searchDone">
<router-link class="previous" v-bind:class="{ 'none-opacity': modelValue === 1 }" :to="{ query: buildPageUrlQuery(modelValue - 1) }">
<translate>Previous page</translate>
<div
v-if="searchDone"
class="pagination"
>
<router-link
class="previous"
:class="{ 'none-opacity': modelValue === 1 }"
:to="{ query: buildPageUrlQuery(modelValue - 1) }"
>
{{ $gettext('Previous page') }}
</router-link>
<div class="pages">
<template v-for="page in pages">
<router-link v-if="page !== modelValue" class="go-to-page" :to="{ query: buildPageUrlQuery(page) }" :key="page">
<template
v-for="page in pages"
:key="page"
>
<router-link
v-if="page !== modelValue"
class="go-to-page"
:to="{ query: buildPageUrlQuery(page) }"
>
{{ page }}
</router-link>
<div v-else class="current">{{ page }}</div>
<div
v-else
class="current"
>
{{ page }}
</div>
</template>
</div>
<router-link class="next" v-bind:class="{ 'none-opacity': modelValue >= maxPage }" :to="{ query: buildPageUrlQuery(+modelValue + 1) }">
<translate>Next page</translate>
<router-link
class="next"
:class="{ 'none-opacity': modelValue >= maxPage }"
:to="{ query: buildPageUrlQuery(+modelValue + 1) }"
>
{{ $gettext('Next page') }}
</router-link>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
maxPage: Number,
searchDone: Boolean,
modelValue: Number,
pages: Array as () => number[]
},
methods: {
buildPageUrlQuery (page: number) {
const query = this.$route.query
return Object.assign({}, query, { page })
},
}
})
</script>
<style lang="scss">
@import '../scss/_variables';
@ -61,26 +101,4 @@
font-size: 14px;
}
}
</style>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
maxPage: Number,
searchDone: Boolean,
modelValue: Number,
pages: Array as () => number[]
},
methods: {
buildPageUrlQuery (page: number) {
const query = this.$route.query
return Object.assign({}, query, { page })
},
}
})
</script>

View File

@ -1,9 +1,17 @@
<template>
<div class="playlist root-result">
<div class="thumbnail">
<a class="img" :title="watchMessage" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="playlist.url">
<img v-bind:src="playlist.thumbnailUrl" alt="">
<a
class="img"
:title="watchMessage"
target="_blank"
rel="nofollow noreferrer noopener"
:href="playlist.url"
>
<img
:src="playlist.thumbnailUrl"
alt=""
>
<span class="videos-length">{{ videosLengthLabel }}</span>
</a>
@ -11,41 +19,105 @@
<div class="information">
<h5 class="title">
<a :title="watchMessage" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="playlist.url">{{ playlist.displayName }}</a>
<a
:title="watchMessage"
target="_blank"
rel="nofollow noreferrer noopener"
:href="playlist.url"
>{{ playlist.displayName }}</a>
</h5>
<div class="description" v-html="renderMarkdown(playlist.description)"></div>
<!-- eslint-disable vue/no-v-html -->
<div
class="description"
v-html="renderMarkdown(playlist.description)"
/>
<!-- eslint-enable -->
<div class="metadatas">
<div class="metadata">
<div class="by-account">
<label v-translate>Created by</label>
<actor-miniature type="account" v-bind:actor="playlist.ownerAccount"></actor-miniature>
<actor-miniature
type="account"
:actor="playlist.ownerAccount"
/>
</div>
<div class="by-channel">
<label v-translate>In</label>
<actor-miniature type="channel" v-bind:actor="playlist.videoChannel"></actor-miniature>
<actor-miniature
type="channel"
:actor="playlist.videoChannel"
/>
</div>
<div class="publishedAt">
<label v-translate>Updated on</label>
<div class="value">{{ updateDate }}</div>
<div class="value">
{{ updateDate }}
</div>
</div>
</div>
<div class="button">
<a class="button-link" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="playlist.url">
<a
class="button-link"
target="_blank"
rel="nofollow noreferrer noopener"
:href="playlist.url"
>
{{ watchMessage }}
</a>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } 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
},
props: {
playlist: Object as PropType<VideoPlaylist>
},
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 })
}
},
methods: {
renderMarkdown(markdown: string) {
return renderMarkdown(markdown)
}
}
})
</script>
<style lang="scss">
@import '../scss/_variables';
@ -100,47 +172,3 @@
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import ActorMiniature from './ActorMiniature.vue'
import { VideoPlaylist } from '../../../PeerTube/shared/models'
import { renderMarkdown } from '../shared/markdown-render'
import { useGettext } from '@jshmrtn/vue3-gettext'
export default defineComponent({
components: {
'actor-miniature': ActorMiniature
},
props: {
playlist: Object as PropType<VideoPlaylist>
},
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 })
}
},
methods: {
renderMarkdown(markdown: string) {
return renderMarkdown(markdown)
}
}
})
</script>

View File

@ -1,6 +1,12 @@
<template>
<div class="block-warning" v-bind:class="{ highlight: highlight }" >
<img src="/img/sepia-warning.svg" alt="">
<div
class="block-warning"
:class="{ highlight: highlight }"
>
<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.
@ -9,6 +15,17 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
indexName: String,
highlight: Boolean
}
})
</script>
<style lang="scss">
@import '../scss/_variables';
@ -44,17 +61,4 @@
display: none !important;
}
}
</style>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
indexName: String,
highlight: Boolean
}
})
</script>

View File

@ -1,124 +1,109 @@
<template>
<div class="video root-result">
<div class="thumbnail">
<a class="img" :title="watchVideoMessage" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="video.url">
<img v-bind:src="getVideoThumbnailUrl()" alt="" @error="setThumbnailError()" :class="{ error: thumbnailError }">
<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" class="live-info" v-translate>LIVE</span>
<span v-else class="duration">{{ formattedDuration }}</span>
<span
v-if="video.isLive"
v-translate
class="live-info"
>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" v-bind:href="video.url">{{ video.name }}</a>
<a
:title="watchVideoMessage"
target="_blank"
rel="nofollow noreferrer noopener"
:href="video.url"
>{{ video.name }}</a>
</h5>
<div class="description" v-html="renderMarkdown(video.description)"></div>
<!-- eslint-disable vue/no-v-html -->
<div
class="description"
v-html="renderMarkdown(video.description)"
/>
<!-- eslint-enable -->
<div class="metadatas">
<div class="metadata">
<div class="by-account">
<label v-translate>Created by</label>
<actor-miniature type="account" v-bind:actor="video.account"></actor-miniature>
<actor-miniature
type="account"
:actor="video.account"
/>
</div>
<div class="by-channel">
<label v-translate>In</label>
<actor-miniature type="channel" v-bind:actor="video.channel"></actor-miniature>
<actor-miniature
type="channel"
:actor="video.channel"
/>
</div>
<div class="publishedAt">
<label v-translate>On</label>
<div class="value">{{ publicationDate }}</div>
<div class="value">
{{ publicationDate }}
</div>
</div>
<div class="language">
<label v-translate>Language</label>
<div class="value">{{ video.language.label }}</div>
<div class="value">
{{ video.language.label }}
</div>
</div>
<div class="tags" v-if="video.tags && video.tags.length !== 0">
<div
v-if="video.tags && video.tags.length !== 0"
class="tags"
>
<label v-translate>Tags</label>
<div class="value">{{ video.tags.join(', ') }}</div>
<div class="value">
{{ video.tags.join(', ') }}
</div>
</div>
</div>
<div class="button">
<a class="button-link" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="video.url">
<a
class="button-link"
target="_blank"
rel="nofollow noreferrer noopener"
:href="video.url"
>
{{ watchVideoMessage }}
</a>
</div>
</div>
</div>
</template>
<style lang="scss">
@import '../scss/_variables';
.video {
.thumbnail {
margin-right: 20px;
--thumbnail-width: #{$thumbnail-width};
--thumbnail-height: #{$thumbnail-height};
// For the duration overlay
.img {
position: relative;
}
img {
background-color: #E5E5E5;
width: var(--thumbnail-width);
height: var(--thumbnail-height);
border-radius: 3px;
&.error {
border: 1px solid #E5E5E5;
}
}
.duration,
.live-info {
position: absolute;
padding: 2px 5px;
right: 5px;
bottom: 5px;
display: inline-block;
color: #fff;
font-size: 11px;
border-radius: 3px;
}
.duration {
background-color: rgba(0, 0, 0, 0.7);
}
.live-info {
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>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import ActorMiniature from './ActorMiniature.vue'
@ -184,3 +169,63 @@
}
})
</script>
<style lang="scss">
@import '../scss/_variables';
.video {
.thumbnail {
margin-right: 20px;
--thumbnail-width: #{$thumbnail-width};
--thumbnail-height: #{$thumbnail-height};
// For the duration overlay
.img {
position: relative;
}
img {
background-color: #E5E5E5;
width: var(--thumbnail-width);
height: var(--thumbnail-height);
border-radius: 3px;
&.error {
border: 1px solid #E5E5E5;
}
}
.duration,
.live-info {
position: absolute;
padding: 2px 5px;
right: 5px;
bottom: 5px;
display: inline-block;
color: #fff;
font-size: 11px;
border-radius: 3px;
}
.duration {
background-color: rgba(0, 0, 0, 0.7);
}
.live-info {
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>

8
client/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <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
}

View File

@ -1,10 +1,10 @@
import './scss/main.scss'
import { createApp } from 'vue'
import VueMatomo from 'vue-matomo'
import { createRouter, createWebHistory } from 'vue-router'
import { createGettext } from 'vue3-gettext'
import App from './App.vue'
import Search from './views/Search.vue'
import { createRouter, createWebHistory } from 'vue-router'
import { createGettext, useGettext } from '@jshmrtn/vue3-gettext'
const app = createApp(App)
@ -42,7 +42,7 @@ const allLocales = Object.keys(availableLanguages).concat(Object.keys(aliasesLan
const defaultLanguage = 'en_US'
let currentLanguage = defaultLanguage
const basePath = process.env.BASE_URL
const basePath = import.meta.env.BASE_URL
const startRegexp = new RegExp('^' + basePath)
const paths = window.location.pathname
@ -111,10 +111,7 @@ buildTranslationsPromise(defaultLanguage, currentLanguage)
})
// Stats Matomo
if (!(navigator.doNotTrack === 'yes' ||
navigator.doNotTrack === '1' ||
window.doNotTrack === '1')
) {
if (!(navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1')) {
app.use(VueMatomo, {
// Configure your matomo server and site
host: 'https://stats.framasoft.org/',
@ -137,26 +134,6 @@ buildTranslationsPromise(defaultLanguage, currentLanguage)
enableLinkTracking: true
})
const _paq = []
// CNIL conformity
_paq.push([ function piwikCNIL () {
// @ts-expect-error
const self = this
function getOriginalVisitorCookieTimeout () {
const now = new Date()
const nowTs = Math.round(now.getTime() / 1000)
const visitorInfo = self.getVisitorInfo()
const createTs = parseInt(visitorInfo[2], 10)
const cookieTimeout = 33696000 // 13 months in seconds
return (createTs + cookieTimeout) - nowTs
}
// @ts-expect-error
this.setVisitorCookieTimeout(getOriginalVisitorCookieTimeout())
} ])
}
app.use(router)

View File

@ -188,7 +188,7 @@ body {
}
}
.metadatas {
.metadata {
> div {
min-height: 27px;
display: flex;
@ -228,7 +228,7 @@ body {
margin: 0 0 20px -10px !important;
}
.metadatas {
.metadata {
label {
min-width: 70px;
margin-right: 10px;

View File

@ -1,4 +1,4 @@
import * as MarkdownIt from 'markdown-it'
import MarkdownIt from 'markdown-it'
const TEXT_RULES = [
'linkify',

View File

@ -1,7 +1,7 @@
function buildApiUrl (path: string) {
const normalizedPath = path.startsWith('/') ? path : '/' + path
const base = process.env.VUE_APP_API_URL || ''
const base = import.meta.env.VITE_APP_API_URL || ''
return base + normalizedPath
}
@ -83,11 +83,11 @@ function publishedDateRangeToAPIParams (publishedDateRange: string) {
return { startDate: date.toISOString(), endDate: undefined }
}
function extractTagsFromQuery (value: any | any[]) {
function extractTagsFromQuery <T> (value: T | T[]) {
if (!value) return []
if (Array.isArray(value) === true) {
return (value as any[]).map(v => ({ text: v }))
if (Array.isArray(value)) {
return value.map(v => ({ text: v }))
}
return [ { text: value } ]

View File

@ -1,11 +0,0 @@
import { Language } from '@jshmrtn/vue3-gettext'
declare module '@vue/runtime-core' {
export interface ComponentCustomProperties {
$gettext: Language['$gettext']
$pgettext: Language['$pgettext']
$ngettext: Language['$ngettext']
$npgettext: Language['$npgettext']
$gettextInterpolate: Language['interpolate']
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,23 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": false,
"noImplicitThis": true,
"jsx": "preserve",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"lib": [
"dom",
"es2015",
"es2016",
"es2017"
],
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"paths": {
"@shared/*": [ "../PeerTube/shared/*" ],
"@/*": [
"src/*"
"./src/*"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.vue",
"tests/**/*.ts"
],
"exclude": [
"node_modules"
]
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

10
client/vite.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 8080
}
})

View File

@ -1,3 +0,0 @@
module.exports = {
publicPath: '/'
}

File diff suppressed because it is too large Load Diff

View File

@ -36,9 +36,8 @@ const apiRoute = '/api/' + API_VERSION
app.use(apiRoute, apiRouter)
// Static client files
app.use('/js/', express.static(join(__dirname, '../client/dist/js'), { maxAge: '30d' }))
app.use('/css/', express.static(join(__dirname, '../client/dist/css'), { maxAge: '30d' }))
app.use('/img/', express.static(join(__dirname, '../client/dist/img'), { maxAge: '30d' }))
app.use('/assets/', express.static(join(__dirname, '../client/dist/assets'), { maxAge: '30d' }))
app.use('/theme/', express.static(join(__dirname, './themes'), { maxAge: '30d' }))
app.use('/opensearch.xml', async function (req, res) {