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. Then open http://localhost:8080.

View File

@ -1,2 +1,2 @@
VUE_APP_API_URL=http://localhost:3234 VITE_APP_API_URL=http://localhost:3234
#VUE_APP_API_URL=https://search.joinpeertube.org #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/**/*~ 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 http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <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> </head>
<body> <body>
@ -15,9 +15,7 @@
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected -->
<div id="footer"></div> <script type="module" src="/src/main.ts"></script>
<!-- built files will be auto injected -->
</body> </body>
</html> </html>

View File

@ -3,34 +3,32 @@
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve --mode development", "dev": "vite",
"build": "vue-cli-service build --mode production", "build": "vue-tsc --noEmit && vite build",
"lint": "vue-cli-service lint", "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" "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": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@jshmrtn/vue3-gettext": "^1.5.0",
"@sipec/vue3-tags-input": "^3.0.4", "@sipec/vue3-tags-input": "^3.0.4",
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/markdown-it": "^10.0.1", "@types/markdown-it": "^12.2.3",
"@vue/cli-plugin-typescript": "~5.0.0-beta.2", "@typescript-eslint/eslint-plugin": "^5.12.1",
"@vue/cli-service": "~5.0.0-beta.2", "@typescript-eslint/parser": "^5.12.1",
"@vue/compiler-sfc": "^3.1.0", "@vitejs/plugin-vue": "^2.2.2",
"axios": "^0.20.0", "@vue/eslint-config-typescript": "^10.0.0",
"markdown-it": "^11.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", "nprogress": "^0.2.0",
"register-service-worker": "^1.0.0", "sass": "^1.49.8",
"sass": "^1.35.1", "typescript": "~4.5.5",
"sass-loader": "^12.1.0", "vite": "^2.8.4",
"typescript": "~4.3.4", "vue": "^3.2.31",
"vue": "^3.1.0", "vue-matomo": "^4.1.0",
"vue-matomo": "^4.0.1", "vue-router": "^4.0.12",
"vue-router": "^4.0.0" "vue-tsc": "^0.31.4",
}, "vue3-gettext": "^2.1.0"
"browserslist": [ }
"> 1%",
"last 2 versions",
"not dead"
]
} }

View File

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

View File

@ -1,6 +1,18 @@
<template> <template>
<a v-bind:href="actor.url" rel="nofollow noreferrer noopener" target="_blank" class="actor" v-bind:title="linkTitle"> <a
<img v-if="actor.avatar && !avatarError" v-bind:src="actor.avatar.url" alt="" :class="{ account: isAccount }" @error="setAvatarError()"> :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> <strong>{{ actor.displayName }}</strong>
@ -10,6 +22,42 @@
</a> </a>
</template> </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"> <style lang="scss">
@import '../scss/_variables'; @import '../scss/_variables';
@ -56,39 +104,3 @@
} }
</style> </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> <template>
<div class="channel root-result"> <div class="channel root-result">
<a
<a target="_blank" rel="nofollow noreferrer noopener" v-bind:href="channel.url" :title="discoverChannelMessage" class="avatar"> target="_blank"
<img v-if="channel.avatar" v-bind:src="channel.avatar.url" alt=""> rel="nofollow noreferrer noopener"
<img v-else src="/img/default-avatar.png" alt=""> :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> </a>
<div class="information"> <div class="information">
<div class="title-block"> <div class="title-block">
<h5 class="title"> <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 }} {{ channel.displayName }}
</a> </a>
</h5> </h5>
@ -23,40 +41,24 @@
</div> </div>
</div> </div>
<div class="description">{{ channel.description }}</div> <div class="description">
{{ channel.description }}
</div>
<div class="button"> <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 }} {{ discoverChannelMessage }}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</template> </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"> <script lang="ts">
import { defineComponent, PropType } from 'vue' import { defineComponent, PropType } from 'vue'
import { VideoChannel } from '../../../PeerTube/shared/models' import { VideoChannel } from '../../../PeerTube/shared/models'
@ -85,3 +87,25 @@
} }
}) })
</script> </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> <template>
<footer id="main-footer"> <footer id="main-footer">
<div class="header"> <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>
<div class="columns"> <div class="columns">
<div> <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> <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> </div>
<div class="big-link"> <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>
<div class="footer"> <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> </div>
</footer> </footer>
</template> </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"> <style lang="scss">
@import '../scss/_variables'; @import '../scss/_variables';
@ -152,22 +217,3 @@
} }
} }
</style> </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> <template>
<div> <div>
<header> <header>
<interface-language-dropdown class="interface-language-dropdown"></interface-language-dropdown> <interface-language-dropdown class="interface-language-dropdown" />
<h1> <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> </h1>
<template v-if="!smallFormat"> <template v-if="!smallFormat">
<h4> <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> </h4>
<div class="search-home"> <div class="search-home">
<img v-if="searchImageUrl" :src="searchImageUrl" alt=""> <img
v-if="searchImageUrl"
:src="searchImageUrl"
alt=""
>
</div> </div>
</template> </template>
</header> </header>
</div> </div>
</template> </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"> <style lang="scss">
@import '../scss/_variables'; @import '../scss/_variables';
@ -108,43 +166,3 @@
} }
} }
</style> </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> <template>
<div class="interface-language-dropdown"> <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"> <div
<a v-for="(lang, locale) in availableLanguages" :key="locale" :href="buildLanguageRoute(locale)" class="menu-item"> v-if="showMenu"
class="menu"
>
<a
v-for="(lang, locale) in availableLanguages"
:key="locale"
:href="buildLanguageRoute(locale)"
class="menu-item"
>
{{ lang }} {{ lang }}
</a> </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 Translate
</a> </a>
</div> </div>
</div> </div>
</template> </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"> <style lang="scss">
@import '../scss/_variables'; @import '../scss/_variables';
@ -67,44 +126,3 @@
} }
} }
</style> </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> <template>
<div
<div class="pagination" v-if="searchDone"> v-if="searchDone"
<router-link class="previous" v-bind:class="{ 'none-opacity': modelValue === 1 }" :to="{ query: buildPageUrlQuery(modelValue - 1) }"> class="pagination"
<translate>Previous page</translate> >
<router-link
class="previous"
:class="{ 'none-opacity': modelValue === 1 }"
:to="{ query: buildPageUrlQuery(modelValue - 1) }"
>
{{ $gettext('Previous page') }}
</router-link> </router-link>
<div class="pages"> <div class="pages">
<template v-for="page in pages"> <template
v-for="page in pages"
<router-link v-if="page !== modelValue" class="go-to-page" :to="{ query: buildPageUrlQuery(page) }" :key="page"> :key="page"
>
<router-link
v-if="page !== modelValue"
class="go-to-page"
:to="{ query: buildPageUrlQuery(page) }"
>
{{ page }} {{ page }}
</router-link> </router-link>
<div v-else class="current">{{ page }}</div> <div
v-else
class="current"
>
{{ page }}
</div>
</template> </template>
</div> </div>
<router-link class="next" v-bind:class="{ 'none-opacity': modelValue >= maxPage }" :to="{ query: buildPageUrlQuery(+modelValue + 1) }"> <router-link
<translate>Next page</translate> class="next"
:class="{ 'none-opacity': modelValue >= maxPage }"
:to="{ query: buildPageUrlQuery(+modelValue + 1) }"
>
{{ $gettext('Next page') }}
</router-link> </router-link>
</div> </div>
</template> </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"> <style lang="scss">
@import '../scss/_variables'; @import '../scss/_variables';
@ -61,26 +101,4 @@
font-size: 14px; font-size: 14px;
} }
} }
</style> </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> <template>
<div class="playlist root-result"> <div class="playlist root-result">
<div class="thumbnail"> <div class="thumbnail">
<a class="img" :title="watchMessage" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="playlist.url"> <a
<img v-bind:src="playlist.thumbnailUrl" alt=""> class="img"
:title="watchMessage"
target="_blank"
rel="nofollow noreferrer noopener"
:href="playlist.url"
>
<img
:src="playlist.thumbnailUrl"
alt=""
>
<span class="videos-length">{{ videosLengthLabel }}</span> <span class="videos-length">{{ videosLengthLabel }}</span>
</a> </a>
@ -11,41 +19,105 @@
<div class="information"> <div class="information">
<h5 class="title"> <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> </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"> <div class="by-account">
<label v-translate>Created by</label> <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>
<div class="by-channel"> <div class="by-channel">
<label v-translate>In</label> <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>
<div class="publishedAt"> <div class="publishedAt">
<label v-translate>Updated on</label> <label v-translate>Updated on</label>
<div class="value">{{ updateDate }}</div> <div class="value">
{{ updateDate }}
</div>
</div> </div>
</div> </div>
<div class="button"> <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 }} {{ watchMessage }}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</template> </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"> <style lang="scss">
@import '../scss/_variables'; @import '../scss/_variables';
@ -100,47 +172,3 @@
} }
} }
</style> </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> <template>
<div class="block-warning" v-bind:class="{ highlight: highlight }" > <div
<img src="/img/sepia-warning.svg" alt=""> class="block-warning"
:class="{ highlight: highlight }"
>
<img
src="/img/sepia-warning.svg"
alt=""
>
<div v-translate> <div v-translate>
<strong>%{indexName}</strong> displays videos and channels that match your search but is not the publisher, nor the owner. <strong>%{indexName}</strong> displays videos and channels that match your search but is not the publisher, nor the owner.
@ -9,6 +15,17 @@
</div> </div>
</template> </template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
indexName: String,
highlight: Boolean
}
})
</script>
<style lang="scss"> <style lang="scss">
@import '../scss/_variables'; @import '../scss/_variables';
@ -44,17 +61,4 @@
display: none !important; display: none !important;
} }
} }
</style> </style>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
indexName: String,
highlight: Boolean
}
})
</script>

View File

@ -1,124 +1,109 @@
<template> <template>
<div class="video root-result"> <div class="video root-result">
<div class="thumbnail"> <div class="thumbnail">
<a class="img" :title="watchVideoMessage" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="video.url"> <a
<img v-bind:src="getVideoThumbnailUrl()" alt="" @error="setThumbnailError()" :class="{ error: thumbnailError }"> 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
<span v-else class="duration">{{ formattedDuration }}</span> v-if="video.isLive"
v-translate
class="live-info"
>LIVE</span>
<span
v-else
class="duration"
>{{ formattedDuration }}</span>
</a> </a>
</div> </div>
<div class="information"> <div class="information">
<h5 class="title"> <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> </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"> <div class="by-account">
<label v-translate>Created by</label> <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>
<div class="by-channel"> <div class="by-channel">
<label v-translate>In</label> <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>
<div class="publishedAt"> <div class="publishedAt">
<label v-translate>On</label> <label v-translate>On</label>
<div class="value">{{ publicationDate }}</div> <div class="value">
{{ publicationDate }}
</div>
</div> </div>
<div class="language"> <div class="language">
<label v-translate>Language</label> <label v-translate>Language</label>
<div class="value">{{ video.language.label }}</div> <div class="value">
{{ video.language.label }}
</div>
</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> <label v-translate>Tags</label>
<div class="value">{{ video.tags.join(', ') }}</div> <div class="value">
{{ video.tags.join(', ') }}
</div>
</div> </div>
</div> </div>
<div class="button"> <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 }} {{ watchVideoMessage }}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</template> </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"> <script lang="ts">
import { defineComponent, PropType } from 'vue' import { defineComponent, PropType } from 'vue'
import ActorMiniature from './ActorMiniature.vue' import ActorMiniature from './ActorMiniature.vue'
@ -184,3 +169,63 @@
} }
}) })
</script> </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 './scss/main.scss'
import { createApp } from 'vue' import { createApp } from 'vue'
import VueMatomo from 'vue-matomo' import VueMatomo from 'vue-matomo'
import { createRouter, createWebHistory } from 'vue-router'
import { createGettext } from 'vue3-gettext'
import App from './App.vue' import App from './App.vue'
import Search from './views/Search.vue' import Search from './views/Search.vue'
import { createRouter, createWebHistory } from 'vue-router'
import { createGettext, useGettext } from '@jshmrtn/vue3-gettext'
const app = createApp(App) const app = createApp(App)
@ -42,7 +42,7 @@ const allLocales = Object.keys(availableLanguages).concat(Object.keys(aliasesLan
const defaultLanguage = 'en_US' const defaultLanguage = 'en_US'
let currentLanguage = defaultLanguage let currentLanguage = defaultLanguage
const basePath = process.env.BASE_URL const basePath = import.meta.env.BASE_URL
const startRegexp = new RegExp('^' + basePath) const startRegexp = new RegExp('^' + basePath)
const paths = window.location.pathname const paths = window.location.pathname
@ -111,10 +111,7 @@ buildTranslationsPromise(defaultLanguage, currentLanguage)
}) })
// Stats Matomo // Stats Matomo
if (!(navigator.doNotTrack === 'yes' || if (!(navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1')) {
navigator.doNotTrack === '1' ||
window.doNotTrack === '1')
) {
app.use(VueMatomo, { app.use(VueMatomo, {
// Configure your matomo server and site // Configure your matomo server and site
host: 'https://stats.framasoft.org/', host: 'https://stats.framasoft.org/',
@ -137,26 +134,6 @@ buildTranslationsPromise(defaultLanguage, currentLanguage)
enableLinkTracking: true 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) app.use(router)

View File

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

View File

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

View File

@ -1,7 +1,7 @@
function buildApiUrl (path: string) { function buildApiUrl (path: string) {
const normalizedPath = path.startsWith('/') ? path : '/' + path 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 return base + normalizedPath
} }
@ -83,11 +83,11 @@ function publishedDateRangeToAPIParams (publishedDateRange: string) {
return { startDate: date.toISOString(), endDate: undefined } return { startDate: date.toISOString(), endDate: undefined }
} }
function extractTagsFromQuery (value: any | any[]) { function extractTagsFromQuery <T> (value: T | T[]) {
if (!value) return [] if (!value) return []
if (Array.isArray(value) === true) { if (Array.isArray(value)) {
return (value as any[]).map(v => ({ text: v })) return value.map(v => ({ text: v }))
} }
return [ { text: value } ] 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": { "compilerOptions": {
"target": "esnext", "target": "esnext",
"useDefineForClassFields": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "node",
"strict": false, "strict": false,
"noImplicitThis": true, "noImplicitThis": true,
"jsx": "preserve", "jsx": "preserve",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "resolveJsonModule": true,
"lib": [ "esModuleInterop": true,
"dom", "lib": ["esnext", "dom"],
"es2015",
"es2016",
"es2017"
],
"paths": { "paths": {
"@shared/*": [ "../PeerTube/shared/*" ], "@shared/*": [ "../PeerTube/shared/*" ],
"@/*": [ "@/*": [
"src/*" "./src/*"
] ]
} }
}, },
"include": [ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"src/**/*.ts", "references": [{ "path": "./tsconfig.node.json" }]
"src/**/*.vue",
"tests/**/*.ts"
],
"exclude": [
"node_modules"
]
} }

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) app.use(apiRoute, apiRouter)
// Static client files // 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('/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('/theme/', express.static(join(__dirname, './themes'), { maxAge: '30d' }))
app.use('/opensearch.xml', async function (req, res) { app.use('/opensearch.xml', async function (req, res) {