LINT
This commit is contained in:
parent
820ceda499
commit
adcbfcb1be
@ -27,4 +27,4 @@ export const getBadgesFromMessage = (tags: any, allBadges: Badge[]) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return getBadges(allBadges, formatedBadges)
|
return getBadges(allBadges, formatedBadges)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { getBadgesFromMessage } from './badges'
|
|||||||
export function parseMessage(messageData: any, allBadges: Badge[]): ParsedMessage {
|
export function parseMessage(messageData: any, allBadges: Badge[]): ParsedMessage {
|
||||||
const message = JSON.parse(messageData)
|
const message = JSON.parse(messageData)
|
||||||
|
|
||||||
if (message.type === undefined && message.cursor !== "") {
|
if (message.type === undefined && message.cursor !== '') {
|
||||||
const data: ParsedMessage = {
|
const data: ParsedMessage = {
|
||||||
type: 'PRIVMSG',
|
type: 'PRIVMSG',
|
||||||
data: {
|
data: {
|
||||||
@ -23,9 +23,9 @@ export function parseMessage(messageData: any, allBadges: Badge[]): ParsedMessag
|
|||||||
case 'PRIVMSG': {
|
case 'PRIVMSG': {
|
||||||
const data: ParsedMessage = {
|
const data: ParsedMessage = {
|
||||||
type: 'PRIVMSG',
|
type: 'PRIVMSG',
|
||||||
data: {
|
data: {
|
||||||
message: message.message,
|
message: message.message,
|
||||||
username: message.username,
|
username: message.username,
|
||||||
color: message.tags.color,
|
color: message.tags.color,
|
||||||
badges: getBadgesFromMessage(message.tags, allBadges)
|
badges: getBadgesFromMessage(message.tags, allBadges)
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export const createQualitySelector = (player: any) => {
|
|||||||
const MenuItem = videojs.getComponent('MenuItem')
|
const MenuItem = videojs.getComponent('MenuItem')
|
||||||
let formatedQualities: { name: string; index: number; id: string }[]
|
let formatedQualities: { name: string; index: number; id: string }[]
|
||||||
|
|
||||||
let t = i18n.global.t
|
const t = i18n.global.t
|
||||||
|
|
||||||
const setQuality = (id: string) => {
|
const setQuality = (id: string) => {
|
||||||
const found = formatedQualities.find((i) => i.id === id)
|
const found = formatedQualities.find((i) => i.id === id)
|
||||||
@ -56,10 +56,10 @@ export const createQualitySelector = (player: any) => {
|
|||||||
videojs.registerComponent('CustomMenuButton', CustomMenuButton)
|
videojs.registerComponent('CustomMenuButton', CustomMenuButton)
|
||||||
const formattedLevels = []
|
const formattedLevels = []
|
||||||
|
|
||||||
const updateLevels = (items: { name: string; index: number; id: string; }[]) => {
|
const updateLevels = () => {
|
||||||
player.controlBar.removeChild('CustomMenuButton')
|
player.controlBar.removeChild('CustomMenuButton')
|
||||||
player.controlBar.addChild('CustomMenuButton', {
|
player.controlBar.addChild('CustomMenuButton', {
|
||||||
title: t("player.quality"),
|
title: t('player.quality'),
|
||||||
items: formatedQualities
|
items: formatedQualities
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export * from './Badge'
|
export * from './Badge'
|
||||||
export * from './ParsedMessage'
|
export * from './ParsedMessage'
|
||||||
|
@ -1,48 +1,50 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
categoryData: {
|
categoryData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
return {
|
|
||||||
category: props.categoryData
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
abbreviate(text: number) {
|
|
||||||
return Intl.NumberFormat('en-US', {
|
|
||||||
//@ts-ignore
|
|
||||||
notation: 'compact',
|
|
||||||
maximumFractionDigits: 1
|
|
||||||
}).format(text)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
return {
|
||||||
|
category: props.categoryData
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
abbreviate(text: number) {
|
||||||
|
return Intl.NumberFormat('en-US', {
|
||||||
|
//@ts-ignore
|
||||||
|
notation: 'compact',
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
}).format(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-ctp-crust w-40 lg:w-[11rem] md:w-[13.5rem] rounded-lg">
|
<div class="bg-ctp-crust w-40 lg:w-[11rem] md:w-[13.5rem] rounded-lg">
|
||||||
<router-link :to="`/game/${category.name}`">
|
<router-link :to="`/game/${category.name}`">
|
||||||
<img :src="category.image" class="rounded-lg rounded-b-none w-full" />
|
<img :src="category.image" class="rounded-lg rounded-b-none w-full" />
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-bold text-white text-xl sm:text-base md:text-xl">
|
<p class="font-bold text-white text-xl sm:text-base md:text-xl">
|
||||||
{{ category.displayName }}
|
{{ category.displayName }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-white">{{ abbreviate(category.viewers) }} viewers</p>
|
<p class="text-sm text-white">{{ abbreviate(category.viewers) }} viewers</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="h-8 overflow-hidden">
|
<ul class="h-8 overflow-hidden">
|
||||||
<li v-for="tag in category.tags" :key="tag" class="inline-flex">
|
<li v-for="tag in category.tags" :key="tag" class="inline-flex">
|
||||||
<span class="p-2.5 py-1.5 bg-ctp-surface0 rounded-md m-0.5 text-xs font-bold text-white">{{ tag
|
<span
|
||||||
}}</span>
|
class="p-2.5 py-1.5 bg-ctp-surface0 rounded-md m-0.5 text-xs font-bold text-white"
|
||||||
</li>
|
>{{ tag }}</span
|
||||||
</ul>
|
>
|
||||||
</div>
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</template>
|
||||||
|
@ -1,31 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
channel: {
|
channel: {
|
||||||
type: Object
|
type: Object
|
||||||
}
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
return {
|
|
||||||
channelData: props.channel
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
return {
|
||||||
|
channelData: props.channel
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-link v-if="channelData" :to="'/' + channelData.username">
|
<router-link v-if="channelData" :to="'/' + channelData.username">
|
||||||
<div class="p-3 rounded-lg bg-ctp-crust w-max max-w-lg max-h-28">
|
<div class="p-3 rounded-lg bg-ctp-crust w-max max-w-lg max-h-28">
|
||||||
<div class="inline-flex space-x-3">
|
<div class="inline-flex space-x-3">
|
||||||
<img :src="channelData.pfp" class="rounded-full w-20">
|
<img :src="channelData.pfp" class="rounded-full w-20" />
|
||||||
<div>
|
<div>
|
||||||
<div class="inline-flex w-full justify-between">
|
<div class="inline-flex w-full justify-between">
|
||||||
<h1 class="text-white text-3xl font-bold">{{ channelData.username }}</h1>
|
<h1 class="text-white text-3xl font-bold">{{ channelData.username }}</h1>
|
||||||
<p class="text-white float-right ml-5">{{ channelData.followers }} followers</p>
|
<p class="text-white float-right ml-5">{{ channelData.followers }} followers</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-white overflow-y-hidden overflow-ellipsis max-h-12">{{ channelData.about }}</p>
|
<p class="text-white overflow-y-hidden overflow-ellipsis max-h-12">
|
||||||
</div>
|
{{ channelData.about }}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rounded-lg z-50 flex m-3 h-20 bg-amber-400 border-4 border-amber-600">
|
<div class="rounded-lg z-50 flex m-3 h-20 bg-amber-400 border-4 border-amber-600">
|
||||||
<div class="m-auto">
|
<div class="m-auto">
|
||||||
<h1 class="font-bold text-2xl">SafeTwitch is currently in development mode.</h1>
|
<h1 class="font-bold text-2xl">SafeTwitch is currently in development mode.</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -7,12 +7,12 @@ export default {}
|
|||||||
class="flex flex-col max-w-prose justify-center text-center mx-auto p-6 bg-ctp-crust rounded-lg text-white"
|
class="flex flex-col max-w-prose justify-center text-center mx-auto p-6 bg-ctp-crust rounded-lg text-white"
|
||||||
>
|
>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h1 class="font-bold text-5xl">{{ $t("error.oops") }}</h1>
|
<h1 class="font-bold text-5xl">{{ $t('error.oops') }}</h1>
|
||||||
<p class="font-bold text-3xl">{{ $t("error.notsupposedtohappen") }}</p>
|
<p class="font-bold text-3xl">{{ $t('error.notsupposedtohappen') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-xl">
|
<p class="text-xl">
|
||||||
{{ $t("error.serverexplain") }}
|
{{ $t('error.serverexplain') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -56,7 +56,7 @@ export default {
|
|||||||
class="text-white text-sm font-bold p-2 py-1 rounded-md bg-purple-600"
|
class="text-white text-sm font-bold p-2 py-1 rounded-md bg-purple-600"
|
||||||
>
|
>
|
||||||
<v-icon name="bi-heart-fill" scale="0.85"></v-icon>
|
<v-icon name="bi-heart-fill" scale="0.85"></v-icon>
|
||||||
<span v-if="isFollowing"> {{ $t("streamer.unfollow") }} </span>
|
<span v-if="isFollowing"> {{ $t('streamer.unfollow') }} </span>
|
||||||
<span v-else> {{ $t("streamer.follow") }} </span>
|
<span v-else> {{ $t('streamer.follow') }} </span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -5,14 +5,14 @@ export default {
|
|||||||
setup() {
|
setup() {
|
||||||
const version = inject('version')
|
const version = inject('version')
|
||||||
return {
|
return {
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="m-2 mt-5 flex justify-center">
|
<div class="m-2 mt-5 flex justify-center">
|
||||||
<p class="text-white font-bold">SafeTwitch v{{ version }}</p>
|
<p class="text-white font-bold">SafeTwitch v{{ version }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,32 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<select v-model="$i18n.locale" @change="onChange()" class="my-auto p-0 pr-9 bg-transparent border-0" :selected="$i18n.locale">
|
<select
|
||||||
<option v-for="(lang, i) in langs" :key="`Lang${i}`" :value="lang">
|
v-model="$i18n.locale"
|
||||||
{{ names[i] }}
|
@change="onChange()"
|
||||||
</option>
|
class="my-auto p-0 pr-9 bg-transparent border-0"
|
||||||
</select>
|
:selected="$i18n.locale"
|
||||||
</div>
|
>
|
||||||
|
<option v-for="(lang, i) in langs" :key="`Lang${i}`" :value="lang">
|
||||||
|
{{ names[i] }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
langs: ['en-US', 'es-ES', 'nl-NL', 'pt-PT', 'fa-IR', 'he-IL'],
|
langs: ['en-US', 'es-ES', 'nl-NL', 'pt-PT', 'fa-IR', 'he-IL'],
|
||||||
names: ['English', 'Español', 'Nederlands', 'Português', 'فارسی', 'עִבְרִית']
|
names: ['English', 'Español', 'Nederlands', 'Português', 'فارسی', 'עִבְרִית']
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const savedLocale = localStorage.getItem("language")
|
|
||||||
if (savedLocale != null && this.langs.includes(savedLocale)) {
|
|
||||||
this.$i18n.locale = savedLocale
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onChange() {
|
|
||||||
localStorage.setItem("language", this.$i18n.locale)
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const savedLocale = localStorage.getItem('language')
|
||||||
|
if (savedLocale != null && this.langs.includes(savedLocale)) {
|
||||||
|
this.$i18n.locale = savedLocale
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange() {
|
||||||
|
localStorage.setItem('language', this.$i18n.locale)
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex mx-auto justify-center bg-ctp-crust rounded-lg w-2/3 p-2 text-white">
|
<div class="flex mx-auto justify-center bg-ctp-crust rounded-lg w-2/3 p-2 text-white">
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<h1 class="text-4xl font-bold">{{ $t("main.searching") }}</h1>
|
<h1 class="text-4xl font-bold">{{ $t('main.searching') }}</h1>
|
||||||
<v-icon name="fa-circle-notch" class="animate-spin w-10 h-10"></v-icon>
|
<v-icon name="fa-circle-notch" class="animate-spin w-10 h-10"></v-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,8 +20,8 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="inline-flex space-x-6 font-medium">
|
<ul class="inline-flex space-x-6 font-medium">
|
||||||
<a href="https://codeberg.org/dragongoose/safetwitch">{{ $t("nav.code") }}</a>
|
<a href="https://codeberg.org/dragongoose/safetwitch">{{ $t('nav.code') }}</a>
|
||||||
<router-link to="/privacy">{{ $t("nav.privacy") }}</router-link>
|
<router-link to="/privacy">{{ $t('nav.privacy') }}</router-link>
|
||||||
<language-switcher></language-switcher>
|
<language-switcher></language-switcher>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,30 +2,30 @@
|
|||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
searchInput: "",
|
searchInput: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
redirectToSearch() {
|
redirectToSearch() {
|
||||||
const query = this.searchInput
|
const query = this.searchInput
|
||||||
this.$router.push({ path: '/search/', query: { query }})
|
this.$router.push({ path: '/search/', query: { query } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative hidden md:block">
|
<div class="relative hidden md:block">
|
||||||
<label for="searchBar" class="hidden">{{ $t("main.search") }}</label>
|
<label for="searchBar" class="hidden">{{ $t('main.search') }}</label>
|
||||||
<v-icon name="io-search-outline" class="absolute my-auto inset-y-0 left-2"></v-icon>
|
<v-icon name="io-search-outline" class="absolute my-auto inset-y-0 left-2"></v-icon>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="searchBar"
|
id="searchBar"
|
||||||
name="searchBar"
|
name="searchBar"
|
||||||
:placeholder="$t('main.search')"
|
:placeholder="$t('main.search')"
|
||||||
v-model="searchInput"
|
v-model="searchInput"
|
||||||
@keyup.enter=redirectToSearch
|
@keyup.enter="redirectToSearch"
|
||||||
class="rounded-md p-1 pl-8 text-black bg-white placeholder:text-black"
|
class="rounded-md p-1 pl-8 text-black bg-white placeholder:text-black"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ref, inject, provide } from 'vue'
|
import { ref, inject } from 'vue'
|
||||||
|
|
||||||
import BadgeVue from './ChatBadge.vue'
|
import BadgeVue from './ChatBadge.vue'
|
||||||
import { getBadges } from '@/assets/badges'
|
import { getBadges } from '@/assets/badges'
|
||||||
|
@ -35,11 +35,10 @@ export default {
|
|||||||
const emit = this.$emit
|
const emit = this.$emit
|
||||||
this.player = videojs('video-player', this.options, () => {
|
this.player = videojs('video-player', this.options, () => {
|
||||||
createQualitySelector(this.player)
|
createQualitySelector(this.player)
|
||||||
let i = 0
|
|
||||||
|
|
||||||
this.player.on('timeupdate', () => {
|
this.player.on('timeupdate', () => {
|
||||||
emit('PlayerTimeUpdate', this.player.currentTime())
|
emit('PlayerTimeUpdate', this.player.currentTime())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
|
@ -1,49 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-w-[300px]">
|
<div class="min-w-[300px]">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<RouterLink :to="'/videos/' + data.id">
|
<RouterLink :to="'/videos/' + videoData.id">
|
||||||
<img :src="data.preview" class="rounded-md" width="300">
|
<img :src="videoData.preview" class="rounded-md" width="300" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<p class="absolute bottom-2 right-2 bg-black p-1 py-0.5 rounded-md bg-opacity-70 text-xs font-bold"> {{ new Date(data.duration * 1000).toISOString().slice(11, 19) }}</p>
|
<p
|
||||||
<p class="absolute bottom-2 left-2 bg-black p-1 py-0.5 rounded-md bg-opacity-70 text-xs font-bold"> {{ abbreviate(data.views) }} {{ $t("main.views") }}</p>
|
class="absolute bottom-2 right-2 bg-black p-1 py-0.5 rounded-md bg-opacity-70 text-xs font-bold"
|
||||||
</div>
|
>
|
||||||
|
{{ new Date(videoData.duration * 1000).toISOString().slice(11, 19) }}
|
||||||
<div class="pt-2 space-x-2">
|
</p>
|
||||||
<div class="space-x-2 inline-flex">
|
<p
|
||||||
<RouterLink :to="'/game/' + data.game.name">
|
class="absolute bottom-2 left-2 bg-black p-1 py-0.5 rounded-md bg-opacity-70 text-xs font-bold"
|
||||||
<img :src="data.game.image">
|
>
|
||||||
</RouterLink>
|
{{ abbreviate(videoData.views) }} {{ $t('main.views') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="w-full">
|
|
||||||
<p class="font-bold text-sm truncate h-6 max-w-[255px]">{{ data.title }}</p>
|
|
||||||
<div class="text-xs text-gray-400">
|
|
||||||
<p>{{ data.streamer.login }}</p>
|
|
||||||
<p>{{ data.game.displayName || data.game.name }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2 space-x-2">
|
||||||
|
<div class="space-x-2 inline-flex">
|
||||||
|
<RouterLink :to="'/game/' + videoData.game.name">
|
||||||
|
<img :src="videoData.game.image" />
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<p class="font-bold text-sm truncate h-6 max-w-[255px]">{{ data.title }}</p>
|
||||||
|
<div class="text-xs text-gray-400">
|
||||||
|
<p>{{ videoData.streamer.login }}</p>
|
||||||
|
<p>{{ videoData.game.displayName || videoData.game.name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Video } from '@/types/VOD';
|
import type { Video } from '@/types/VOD'
|
||||||
import { abbreviate } from '@/mixins';
|
import { abbreviate } from '@/mixins'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
data: Object
|
data: Object
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
return {
|
return {
|
||||||
data: props.data as Video
|
videoData: props.data as Video
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
abbreviate
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
abbreviate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,26 +3,32 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
||||||
<div class="inline-flex w-full">
|
<div class="inline-flex w-full">
|
||||||
<span class="pr-3 font-bold text-3xl">Videos</span>
|
<span class="pr-3 font-bold text-3xl">Videos</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 v-if="!shelves && status === 'error'">Error getting videos</h1>
|
|
||||||
|
|
||||||
<div v-else-if="shelves" class="mb-5">
|
|
||||||
<div class="space-y-5">
|
|
||||||
<div v-for="shelve of shelves">
|
|
||||||
<h1 class="font-bold text-lg">{{ shelve.title }}</h1>
|
|
||||||
|
|
||||||
<div class="whitespace-nowrap overflow-x-auto overflow-y-hidden w-full inline-flex space-x-5">
|
|
||||||
<video-preview v-for="video of shelve.videos" :data="video"></video-preview>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h1 v-if="!shelves && status === 'error'">Error getting videos</h1>
|
||||||
|
|
||||||
|
<div v-else-if="shelves" class="mb-5">
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div v-for="shelve of shelves" :key="shelve.title">
|
||||||
|
<h1 class="font-bold text-lg">{{ shelve.title }}</h1>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="whitespace-nowrap overflow-x-auto overflow-y-hidden w-full inline-flex space-x-5"
|
||||||
|
>
|
||||||
|
<video-preview
|
||||||
|
v-for="video of shelve.videos"
|
||||||
|
:key="video.title"
|
||||||
|
:data="video"
|
||||||
|
></video-preview>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -33,25 +39,25 @@ import type { Shelve } from '@/types/VOD'
|
|||||||
import VideoPreview from '@/components/user/VideoPreview.vue'
|
import VideoPreview from '@/components/user/VideoPreview.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
shelves: ref<Shelve[]>([]),
|
shelves: ref<Shelve[]>([]),
|
||||||
status: ""
|
status: ''
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
const username = this.$route.params.username
|
|
||||||
|
|
||||||
await getEndpoint("api/vods/shelve/" + username)
|
|
||||||
.then((data) => {
|
|
||||||
this.shelves = data
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.status = "error"
|
|
||||||
})
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
VideoPreview
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
const username = this.$route.params.username
|
||||||
|
|
||||||
|
await getEndpoint('api/vods/shelve/' + username)
|
||||||
|
.then((data) => {
|
||||||
|
this.shelves = data
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = 'error'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
VideoPreview
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -17,6 +17,6 @@ export default createI18n({
|
|||||||
'nl-NL': nl,
|
'nl-NL': nl,
|
||||||
'pt-PT': pt,
|
'pt-PT': pt,
|
||||||
'fa-IR': fa,
|
'fa-IR': fa,
|
||||||
'he-IL': he,
|
'he-IL': he
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -2,9 +2,8 @@ import { createApp } from 'vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import './assets/index.css'
|
import './assets/index.css'
|
||||||
import i18n from "./i18n"
|
import i18n from './i18n'
|
||||||
import { version } from "../package.json"
|
import { version } from '../package.json'
|
||||||
|
|
||||||
|
|
||||||
const app = createApp(App).use(i18n)
|
const app = createApp(App).use(i18n)
|
||||||
|
|
||||||
@ -12,7 +11,7 @@ const app = createApp(App).use(i18n)
|
|||||||
// For some reason, import.meta.env.VITE_HTTPS === "true"
|
// For some reason, import.meta.env.VITE_HTTPS === "true"
|
||||||
// returns false, even if it is true.
|
// returns false, even if it is true.
|
||||||
// Making a copy of the variable seems to work
|
// Making a copy of the variable seems to work
|
||||||
const https = (import.meta.env.SAFETWITCH_HTTPS.slice() === "true")
|
const https = import.meta.env.SAFETWITCH_HTTPS.slice() === 'true'
|
||||||
|
|
||||||
const protocol = https ? 'https://' : 'http://'
|
const protocol = https ? 'https://' : 'http://'
|
||||||
const wsProtocol = https ? 'wss://' : 'ws://'
|
const wsProtocol = https ? 'wss://' : 'ws://'
|
||||||
@ -51,4 +50,4 @@ addIcons(
|
|||||||
|
|
||||||
app.component('v-icon', OhVueIcon)
|
app.component('v-icon', OhVueIcon)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@ -1,48 +1,42 @@
|
|||||||
export function truncate(value: string, length: number) {
|
export function truncate(value: string, length: number) {
|
||||||
if (value.length > length) {
|
if (value.length > length) {
|
||||||
return value.substring(0, length) + '...'
|
return value.substring(0, length) + '...'
|
||||||
} else {
|
} else {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let language = localStorage.getItem("language") || "en-us"
|
const language = localStorage.getItem('language') || 'en-us'
|
||||||
|
|
||||||
export function abbreviate(text: number) {
|
export function abbreviate(text: number) {
|
||||||
return Intl.NumberFormat(language, {
|
return Intl.NumberFormat(language, {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
notation: 'compact',
|
notation: 'compact',
|
||||||
maximumFractionDigits: 1
|
maximumFractionDigits: 1
|
||||||
}).format(text)
|
}).format(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
const https = (import.meta.env.SAFETWITCH_HTTPS.slice() === "true")
|
const https = import.meta.env.SAFETWITCH_HTTPS.slice() === 'true'
|
||||||
const protocol = https ? 'https://' : 'http://'
|
const protocol = https ? 'https://' : 'http://'
|
||||||
const rootBackendUrl = `${protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/`
|
const rootBackendUrl = `${protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/`
|
||||||
|
|
||||||
export async function getEndpoint(endpoint: string) {
|
export async function getEndpoint(endpoint: string) {
|
||||||
let data
|
const res = await fetch(rootBackendUrl + endpoint, {
|
||||||
|
method: 'GET',
|
||||||
try {
|
headers: {
|
||||||
const res = await fetch(rootBackendUrl + endpoint, {
|
'Accept-Language': language
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
"Accept-Language": language
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const rawData = await res.json()
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw res
|
|
||||||
}
|
|
||||||
if (rawData.status !== 'ok') {
|
|
||||||
throw rawData
|
|
||||||
}
|
|
||||||
|
|
||||||
data = rawData.data
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
const rawData = await res.json()
|
||||||
|
|
||||||
return data
|
if (!res.ok) {
|
||||||
}
|
throw res
|
||||||
|
}
|
||||||
|
if (rawData.status !== 'ok') {
|
||||||
|
throw rawData
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = rawData.data
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import type * as all from "./"
|
|
||||||
|
|
||||||
export interface ApiResponse {
|
export interface ApiResponse {
|
||||||
status: "ok" | "error",
|
status: 'ok' | 'error'
|
||||||
data: any
|
data: any
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
export type Tag = string
|
export type Tag = string
|
||||||
|
|
||||||
export interface CategoryPreview {
|
export interface CategoryPreview {
|
||||||
name: string
|
name: string
|
||||||
displayName: string
|
displayName: string
|
||||||
viewers: number
|
viewers: number
|
||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
createdAt?: Date
|
createdAt?: Date
|
||||||
cursor?: string
|
cursor?: string
|
||||||
image: string
|
image: string
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
import type { Tag } from "./"
|
import type { Tag } from './'
|
||||||
import type { StreamData } from "./"
|
|
||||||
|
|
||||||
export interface CategoryMinifiedStream {
|
export interface CategoryMinifiedStream {
|
||||||
title: string
|
title: string
|
||||||
viewers: number
|
viewers: number
|
||||||
preview: string
|
preview: string
|
||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
cursor: string
|
cursor: string
|
||||||
streamer: {
|
streamer: {
|
||||||
name: string
|
name: string
|
||||||
pfp: string
|
pfp: string
|
||||||
colorHex: string
|
colorHex: string
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface CategoryData {
|
export interface CategoryData {
|
||||||
name: string
|
name: string
|
||||||
cover: string
|
cover: string
|
||||||
description: string
|
description: string
|
||||||
viewers: number
|
viewers: number
|
||||||
followers: number
|
followers: number
|
||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
streams: CategoryMinifiedStream[]
|
streams: CategoryMinifiedStream[]
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
export interface TwitchChatOptions {
|
export interface TwitchChatOptions {
|
||||||
login: {
|
login: {
|
||||||
username: string,
|
username: string
|
||||||
password: string
|
password: string
|
||||||
},
|
}
|
||||||
channels: string[]
|
channels: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageTypes = ['PRIVMSG', 'WHISPER']
|
export const MessageTypes = ['PRIVMSG', 'WHISPER']
|
||||||
export type MessageType = typeof MessageTypes[number];
|
export type MessageType = (typeof MessageTypes)[number]
|
||||||
|
|
||||||
export interface Metadata {
|
export interface Metadata {
|
||||||
username: string
|
username: string
|
||||||
messageType: MessageType
|
messageType: MessageType
|
||||||
channel: string
|
channel: string
|
||||||
message: string
|
message: string
|
||||||
tags: { [k:string]:any }
|
tags: { [k: string]: any }
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export interface Emote {
|
export interface Emote {
|
||||||
name: string,
|
name: string
|
||||||
urls : {
|
urls: {
|
||||||
[k: string]: string
|
[k: string]: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { StreamData, StreamerData } from "./"
|
import type { StreamerData } from './'
|
||||||
import type { CategoryPreview } from "./"
|
import type { CategoryPreview } from './'
|
||||||
|
|
||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
channels: StreamerData[]
|
channels: StreamerData[]
|
||||||
categories: CategoryPreview[]
|
categories: CategoryPreview[]
|
||||||
relatedChannels: StreamerData[]
|
relatedChannels: StreamerData[]
|
||||||
channelsWithTag: StreamerData[]
|
channelsWithTag: StreamerData[]
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
export interface Social {
|
export interface Social {
|
||||||
type: string | null
|
type: string | null
|
||||||
name: string,
|
name: string
|
||||||
link: string
|
link: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StreamData {
|
export interface StreamData {
|
||||||
tags: string[]
|
tags: string[]
|
||||||
title: string
|
title: string
|
||||||
topic: string
|
topic: string
|
||||||
startedAt: number
|
startedAt: number
|
||||||
viewers: number
|
viewers: number
|
||||||
preview: string
|
preview: string
|
||||||
cursor?: string
|
cursor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StreamerData {
|
export interface StreamerData {
|
||||||
username: string
|
username: string
|
||||||
login: string
|
login: string
|
||||||
followers: number
|
followers: number
|
||||||
isLive: boolean
|
isLive: boolean
|
||||||
about: string
|
about: string
|
||||||
socials?: Social[]
|
socials?: Social[]
|
||||||
pfp: string
|
pfp: string
|
||||||
banner: string
|
banner: string
|
||||||
stream?: StreamData | null
|
stream?: StreamData | null
|
||||||
isPartner: boolean | null
|
isPartner: boolean | null
|
||||||
colorHex: string
|
colorHex: string
|
||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,49 @@
|
|||||||
import type { StreamerData } from "./Streamer"
|
import type { StreamerData } from './Streamer'
|
||||||
|
|
||||||
export interface MinifiedCategory {
|
export interface MinifiedCategory {
|
||||||
image: string
|
image: string
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
displayName: string
|
displayName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MinifiedStreamer {
|
export interface MinifiedStreamer {
|
||||||
name: string
|
name: string
|
||||||
login: string
|
login: string
|
||||||
pfp: string
|
pfp: string
|
||||||
colorHex: string
|
colorHex: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Video {
|
export interface Video {
|
||||||
preview: string
|
preview: string
|
||||||
game: MinifiedCategory
|
game: MinifiedCategory
|
||||||
duration: number
|
duration: number
|
||||||
title: string
|
title: string
|
||||||
publishedAt: string
|
publishedAt: string
|
||||||
views: number
|
views: number
|
||||||
tag: string[]
|
tag: string[]
|
||||||
streamer: StreamerData
|
streamer: StreamerData
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Shelve {
|
export interface Shelve {
|
||||||
title: string
|
title: string
|
||||||
videos: Video[]
|
videos: Video[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface VodMessager {
|
export interface VodMessager {
|
||||||
name: string
|
name: string
|
||||||
login: string
|
login: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VodCommentBadge {
|
export interface VodCommentBadge {
|
||||||
version: number
|
version: number
|
||||||
setId: string
|
setId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VodComment {
|
export interface VodComment {
|
||||||
message: string
|
message: string
|
||||||
messager: MinifiedStreamer
|
messager: MinifiedStreamer
|
||||||
offset: number
|
offset: number
|
||||||
cursor: string
|
cursor: string
|
||||||
badges: VodCommentBadge[]
|
badges: VodCommentBadge[]
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,4 @@ export * from './Chat'
|
|||||||
export * from './Category'
|
export * from './Category'
|
||||||
export * from './CategoryData'
|
export * from './CategoryData'
|
||||||
export * from './ApiResponse'
|
export * from './ApiResponse'
|
||||||
export * from './VOD'
|
export * from './VOD'
|
||||||
|
@ -8,12 +8,11 @@ import LoadingScreen from '@/components/LoadingScreen.vue'
|
|||||||
import type { CategoryData } from '@/types'
|
import type { CategoryData } from '@/types'
|
||||||
import { getEndpoint, abbreviate } from '@/mixins'
|
import { getEndpoint, abbreviate } from '@/mixins'
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['protocol'],
|
inject: ['protocol'],
|
||||||
async setup() {
|
async setup() {
|
||||||
let data = ref<CategoryData>()
|
let data = ref<CategoryData>()
|
||||||
let status = ref<"ok" | "error">()
|
let status = ref<'ok' | 'error'>()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
@ -21,13 +20,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await getEndpoint("api/discover/" + this.$route.params.game)
|
await getEndpoint('api/discover/' + this.$route.params.game)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.status = "error"
|
this.status = 'error'
|
||||||
})
|
})
|
||||||
.then((data: CategoryData) => {
|
.then((data: CategoryData) => {
|
||||||
this.data = data
|
this.data = data
|
||||||
})
|
})
|
||||||
|
|
||||||
this.getMoreStreams()
|
this.getMoreStreams()
|
||||||
},
|
},
|
||||||
@ -45,10 +44,11 @@ export default {
|
|||||||
if (!cursor) return
|
if (!cursor) return
|
||||||
|
|
||||||
// get rest of streams from api
|
// get rest of streams from api
|
||||||
const resData = await getEndpoint(`api/discover/${this.$route.params.game}/?cursor=${cursor}`)
|
const resData = await getEndpoint(
|
||||||
.catch((err) => {
|
`api/discover/${this.$route.params.game}/?cursor=${cursor}`
|
||||||
throw err
|
).catch((err) => {
|
||||||
})
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
for (let stream of resData.streams) {
|
for (let stream of resData.streams) {
|
||||||
this.data!.streams.push(stream)
|
this.data!.streams.push(stream)
|
||||||
@ -56,7 +56,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
abbreviate,
|
abbreviate
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StreamPreviewVue,
|
StreamPreviewVue,
|
||||||
@ -80,35 +80,47 @@ export default {
|
|||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
<div>
|
<div>
|
||||||
<div class="inline-flex my-1 space-x-3">
|
<div class="inline-flex my-1 space-x-3">
|
||||||
<p class="font-bold text-white text-lg">{{ $t("main.followers") }}: {{ abbreviate(data.followers) }}</p>
|
<p class="font-bold text-white text-lg">
|
||||||
<p class="font-bold text-white text-lg">{{ $t("main.viewers") }}: {{ abbreviate(data.viewers) }}</p>
|
{{ $t('main.followers') }}: {{ abbreviate(data.followers) }}
|
||||||
|
</p>
|
||||||
|
<p class="font-bold text-white text-lg">
|
||||||
|
{{ $t('main.viewers') }}: {{ abbreviate(data.viewers) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="mb-5">
|
<ul class="mb-5">
|
||||||
<li v-for="tag in data.tags" :key="tag" class="inline-flex">
|
<li v-for="tag in data.tags" :key="tag" class="inline-flex">
|
||||||
<span class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-ctp-overlay1 rounded-sm">{{
|
<span
|
||||||
tag
|
class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-ctp-overlay1 rounded-sm"
|
||||||
}}</span>
|
>{{ tag }}</span
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-md text-gray-400 overflow-y-auto hidden md:block">{{ data.description }}</p>
|
<p class="text-md text-gray-400 overflow-y-auto hidden md:block">
|
||||||
|
{{ data.description }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden">
|
<div class="md:hidden">
|
||||||
<div>
|
<div>
|
||||||
<div class="inline-flex my-1 space-x-3">
|
<div class="inline-flex my-1 space-x-3">
|
||||||
<p class="font-bold text-white text-lg">{{ $t("main.followers") }}: {{ abbreviate(data.followers) }}</p>
|
<p class="font-bold text-white text-lg">
|
||||||
<p class="font-bold text-white text-lg">{{ $t("main.viewers") }}: {{ abbreviate(data.viewers) }}</p>
|
{{ $t('main.followers') }}: {{ abbreviate(data.followers) }}
|
||||||
|
</p>
|
||||||
|
<p class="font-bold text-white text-lg">
|
||||||
|
{{ $t('main.viewers') }}: {{ abbreviate(data.viewers) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="mb-5">
|
<ul class="mb-5">
|
||||||
<li v-for="tag in data.tags" :key="tag" class="inline-flex">
|
<li v-for="tag in data.tags" :key="tag" class="inline-flex">
|
||||||
<span class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-ctp-overlay1 rounded-sm">{{
|
<span
|
||||||
tag
|
class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-ctp-overlay1 rounded-sm"
|
||||||
}}</span>
|
>{{ tag }}</span
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -118,7 +130,11 @@ export default {
|
|||||||
|
|
||||||
<div class="max-w-[58rem] mx-auto">
|
<div class="max-w-[58rem] mx-auto">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="stream in data.streams" class="inline-flex m-2 hover:scale-105 transition-transform">
|
<li
|
||||||
|
v-for="stream in data.streams"
|
||||||
|
:key="stream.title"
|
||||||
|
class="inline-flex m-2 hover:scale-105 transition-transform"
|
||||||
|
>
|
||||||
<StreamPreviewVue :stream="stream"></StreamPreviewVue>
|
<StreamPreviewVue :stream="stream"></StreamPreviewVue>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -13,7 +13,7 @@ export default {
|
|||||||
inject: ['protocol'],
|
inject: ['protocol'],
|
||||||
async setup() {
|
async setup() {
|
||||||
let data = ref<CategoryPreviewInterface[]>()
|
let data = ref<CategoryPreviewInterface[]>()
|
||||||
let status = ref<"ok" | "error">()
|
let status = ref<'ok' | 'error'>()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
@ -57,7 +57,9 @@ export default {
|
|||||||
const cursor = this.data[this.data.length - 1].cursor
|
const cursor = this.data[this.data.length - 1].cursor
|
||||||
if (!cursor) return
|
if (!cursor) return
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${this.protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/api/discover/?cursor=${cursor}`
|
`${this.protocol}${
|
||||||
|
import.meta.env.SAFETWITCH_BACKEND_DOMAIN
|
||||||
|
}/api/discover/?cursor=${cursor}`
|
||||||
)
|
)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error('Failed to fetch data')
|
throw new Error('Failed to fetch data')
|
||||||
@ -81,14 +83,13 @@ export default {
|
|||||||
this.following = []
|
this.following = []
|
||||||
}
|
}
|
||||||
|
|
||||||
await getEndpoint("api/discover")
|
await getEndpoint('api/discover')
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.status = "error"
|
this.status = 'error'
|
||||||
})
|
})
|
||||||
.then((data: CategoryPreviewInterface[]) => {
|
.then((data: CategoryPreviewInterface[]) => {
|
||||||
this.data = data
|
this.data = data
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StreamPreviewVue,
|
StreamPreviewVue,
|
||||||
@ -119,13 +120,13 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<h1 class="font-bold text-5xl text-white">{{ $t("home.discover") }}</h1>
|
<h1 class="font-bold text-5xl text-white">{{ $t('home.discover') }}</h1>
|
||||||
<p class="text-xl text-white">{{ $t("home.discoverDescription") }}</p>
|
<p class="text-xl text-white">{{ $t('home.discoverDescription') }}</p>
|
||||||
|
|
||||||
<div class="pt-5 inline-flex text-white">
|
<div class="pt-5 inline-flex text-white">
|
||||||
<p class="mr-2 font-bold text-white">{{ $t("home.tagDescription") }}</p>
|
<p class="mr-2 font-bold text-white">{{ $t('home.tagDescription') }}</p>
|
||||||
<form class="relative">
|
<form class="relative">
|
||||||
<label for="searchBar" class="hidden">{{ $t("main.search") }}</label>
|
<label for="searchBar" class="hidden">{{ $t('main.search') }}</label>
|
||||||
<v-icon name="io-search-outline" class="absolute my-auto inset-y-0 left-2"></v-icon>
|
<v-icon name="io-search-outline" class="absolute my-auto inset-y-0 left-2"></v-icon>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -143,10 +144,11 @@ export default {
|
|||||||
<ul ref="categoryList">
|
<ul ref="categoryList">
|
||||||
<li
|
<li
|
||||||
v-for="category in data"
|
v-for="category in data"
|
||||||
|
:key="category.name"
|
||||||
ref="categoryItem"
|
ref="categoryItem"
|
||||||
class="inline-flex m-2 hover:scale-105 transition-transform"
|
class="inline-flex m-2 hover:scale-105 transition-transform"
|
||||||
>
|
>
|
||||||
<category-preview :category-data="category"></category-preview>
|
<category-preview :category-data="category"></category-preview>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,8 +4,8 @@ export default {}
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center pt-10 font-bold text-5xl text-white">
|
<div class="flex flex-col items-center pt-10 font-bold text-5xl text-white">
|
||||||
<h1>{{ $t("error.oops") }}</h1>
|
<h1>{{ $t('error.oops') }}</h1>
|
||||||
<h1>{{ $t("error.notfound") }}</h1>
|
<h1>{{ $t('error.notfound') }}</h1>
|
||||||
<h2 class="text-4xl">maybe go <RouterLink to="/" class="text-gray-500">home</RouterLink>?</h2>
|
<h2 class="text-4xl">maybe go <RouterLink to="/" class="text-gray-500">home</RouterLink>?</h2>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -3,22 +3,22 @@ export default {}
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<article
|
<article class="prose prose-invert border-2 bg-ctp-crust rounded-lg mx-auto p-8 pt-10 text-white">
|
||||||
class="prose prose-invert border-2 bg-ctp-crust rounded-lg mx-auto p-8 pt-10 text-white"
|
|
||||||
>
|
|
||||||
<h1>Privacy Policy</h1>
|
<h1>Privacy Policy</h1>
|
||||||
<p>
|
<p>
|
||||||
It's.... kind of empty here.
|
It's.... kind of empty here.
|
||||||
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
No logs are kept. That's it. Nothing is stored from you interacting with the site.
|
<br /><br />
|
||||||
Streamers you follow are stored in your browser's LocalStorage, but never reaches the server.
|
|
||||||
Selected language when using SafeTwitch is sent to the server among every request in order to send the data back in the correct language
|
|
||||||
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
Non-official instances are under their own privacy policy, as they may host SafeTwitch with different practices that may log requests
|
No logs are kept. That's it. Nothing is stored from you interacting with the site. Streamers
|
||||||
|
you follow are stored in your browser's LocalStorage, but never reaches the server. Selected
|
||||||
|
language when using SafeTwitch is sent to the server among every request in order to send the
|
||||||
|
data back in the correct language
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
Non-official instances are under their own privacy policy, as they may host SafeTwitch with
|
||||||
|
different practices that may log requests
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
@ -8,90 +8,102 @@ import StreamPreviewVue from '@/components/StreamPreview.vue'
|
|||||||
import ChannelPreview from '@/components/ChannelPreview.vue'
|
import ChannelPreview from '@/components/ChannelPreview.vue'
|
||||||
|
|
||||||
import { getEndpoint } from '@/mixins'
|
import { getEndpoint } from '@/mixins'
|
||||||
import type { ApiResponse, SearchResult, StreamerData } from '@/types'
|
import type { SearchResult, StreamerData } from '@/types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['protocol'],
|
inject: ['protocol'],
|
||||||
setup() {
|
setup() {
|
||||||
let data = ref<SearchResult>()
|
let data = ref<SearchResult>()
|
||||||
const status = ref<"ok" | "error">()
|
const status = ref<'ok' | 'error'>()
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
status,
|
status
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
await getEndpoint("api/search/?query=" + this.$route.query.query)
|
|
||||||
.catch(() => {
|
|
||||||
this.status = "error"
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
this.data = data as SearchResult
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getStream(channel: StreamerData) {
|
|
||||||
return {
|
|
||||||
...channel.stream,
|
|
||||||
streamer: {
|
|
||||||
name: channel.username,
|
|
||||||
pfp: channel.pfp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
CategoryPreview,
|
|
||||||
ErrorMessage,
|
|
||||||
LoadingScreen,
|
|
||||||
StreamPreviewVue,
|
|
||||||
ChannelPreview
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await getEndpoint('api/search/?query=' + this.$route.query.query)
|
||||||
|
.catch(() => {
|
||||||
|
this.status = 'error'
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
this.data = data as SearchResult
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getStream(channel: StreamerData) {
|
||||||
|
return {
|
||||||
|
...channel.stream,
|
||||||
|
streamer: {
|
||||||
|
name: channel.username,
|
||||||
|
pfp: channel.pfp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
CategoryPreview,
|
||||||
|
ErrorMessage,
|
||||||
|
LoadingScreen,
|
||||||
|
StreamPreviewVue,
|
||||||
|
ChannelPreview
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||||
<error-message v-else-if="status == 'error'"></error-message>
|
<error-message v-else-if="status == 'error'"></error-message>
|
||||||
|
|
||||||
|
|
||||||
<div v-else-if="data" class="p-3 space-y-5">
|
|
||||||
<div v-if="data.channels.length > 0">
|
|
||||||
<h1 class="text-white font-bold text-4xl mb-2">Channels related to "{{ $route.query.query }}"</h1>
|
|
||||||
<ul class="flex overflow-x-scroll overflow-y-hidden">
|
|
||||||
<li v-for="channel in data.channels" class="m-2 hover:scale-105 transition-transform">
|
|
||||||
<channel-preview :channel="channel"></channel-preview>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="data.categories.length > 0">
|
|
||||||
<h1 class="text-white font-bold text-4xl mb-2">Categories related to "{{ $route.query.query }}"</h1>
|
|
||||||
<ul class="flex max-w-[100vw] max-h-[27rem] overflow-x-scroll overflow-y-hidden">
|
|
||||||
<li v-for="category in data.categories" class="m-2 hover:scale-105 transition-transform">
|
|
||||||
<category-preview :category-data="category"></category-preview>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="data.relatedChannels.length > 0">
|
|
||||||
<h1 class="text-white font-bold text-4xl mb-2">Live channels with the tag "{{ $route.query.query }}"</h1>
|
|
||||||
<ul class="flex overflow-x-scroll space-x-5 ">
|
|
||||||
<li v-for="channel in data.relatedChannels">
|
|
||||||
<stream-preview-vue :stream="getStream(channel)"></stream-preview-vue>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div v-if="data.channelsWithTag.length > 0">
|
|
||||||
<h1 class="text-white font-bold text-4xl mb-2">Channels with the tag "{{ $route.query.query }}"</h1>
|
|
||||||
<ul class="inline-flex overflow-y-hidden overflow-x-scroll max-w-[100vw] space-x-5">
|
|
||||||
<li v-for="channel in data.channelsWithTag">
|
|
||||||
<channel-preview :channel="channel"></channel-preview>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
<div v-else-if="data" class="p-3 space-y-5">
|
||||||
|
<div v-if="data.channels.length > 0">
|
||||||
|
<h1 class="text-white font-bold text-4xl mb-2">
|
||||||
|
Channels related to "{{ $route.query.query }}"
|
||||||
|
</h1>
|
||||||
|
<ul class="flex overflow-x-scroll overflow-y-hidden">
|
||||||
|
<li
|
||||||
|
v-for="channel in data.channels"
|
||||||
|
:key="channel.id"
|
||||||
|
class="m-2 hover:scale-105 transition-transform"
|
||||||
|
>
|
||||||
|
<channel-preview :channel="channel"></channel-preview>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
<div v-if="data.categories.length > 0">
|
||||||
|
<h1 class="text-white font-bold text-4xl mb-2">
|
||||||
|
Categories related to "{{ $route.query.query }}"
|
||||||
|
</h1>
|
||||||
|
<ul class="flex max-w-[100vw] max-h-[27rem] overflow-x-scroll overflow-y-hidden">
|
||||||
|
<li
|
||||||
|
v-for="category in data.categories"
|
||||||
|
:key="category.name"
|
||||||
|
class="m-2 hover:scale-105 transition-transform"
|
||||||
|
>
|
||||||
|
<category-preview :category-data="category"></category-preview>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="data.relatedChannels.length > 0">
|
||||||
|
<h1 class="text-white font-bold text-4xl mb-2">
|
||||||
|
Live channels with the tag "{{ $route.query.query }}"
|
||||||
|
</h1>
|
||||||
|
<ul class="flex overflow-x-scroll space-x-5">
|
||||||
|
<li v-for="channel in data.relatedChannels" :key="channel.id">
|
||||||
|
<stream-preview-vue :stream="getStream(channel)"></stream-preview-vue>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="data.channelsWithTag.length > 0">
|
||||||
|
<h1 class="text-white font-bold text-4xl mb-2">
|
||||||
|
Channels with the tag "{{ $route.query.query }}"
|
||||||
|
</h1>
|
||||||
|
<ul class="inline-flex overflow-y-hidden overflow-x-scroll max-w-[100vw] space-x-5">
|
||||||
|
<li v-for="channel in data.channelsWithTag" :key="channel.id">
|
||||||
|
<channel-preview :channel="channel"></channel-preview>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
@ -9,16 +9,16 @@ import FollowButton from '@/components/FollowButton.vue'
|
|||||||
import LoadingScreen from '@/components/LoadingScreen.vue'
|
import LoadingScreen from '@/components/LoadingScreen.vue'
|
||||||
import VideoTab from '@/components/user/VideoTab.vue'
|
import VideoTab from '@/components/user/VideoTab.vue'
|
||||||
|
|
||||||
import type { StreamerData, ApiResponse } from '@/types'
|
import type { StreamerData } from '@/types'
|
||||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ["rootBackendUrl"],
|
inject: ['rootBackendUrl'],
|
||||||
async setup() {
|
async setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const username = route.params.username
|
const username = route.params.username
|
||||||
const data = ref<StreamerData>()
|
const data = ref<StreamerData>()
|
||||||
const status = ref<"ok" | "error">()
|
const status = ref<'ok' | 'error'>()
|
||||||
const rootBackendUrl = inject('rootBackendUrl')
|
const rootBackendUrl = inject('rootBackendUrl')
|
||||||
const videoOptions = {
|
const videoOptions = {
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
@ -35,19 +35,19 @@ export default {
|
|||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
status,
|
status,
|
||||||
videoOptions,
|
videoOptions
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const username = this.$route.params.username
|
const username = this.$route.params.username
|
||||||
|
|
||||||
await getEndpoint("api/users/" + username)
|
await getEndpoint('api/users/' + username)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.data = data
|
this.data = data
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.status = "error"
|
this.status = 'error'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
VideoPlayer,
|
VideoPlayer,
|
||||||
@ -58,28 +58,33 @@ export default {
|
|||||||
VideoTab
|
VideoTab
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
truncate, abbreviate
|
truncate,
|
||||||
|
abbreviate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||||
<error-message v-else-if="status == 'error'"></error-message>
|
<error-message v-else-if="status == 'error'"></error-message>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="data"
|
v-else-if="data"
|
||||||
class="w-full justify-center md:inline-flex space-y-4 md:space-y-0 md:space-x-4 md:p-4"
|
class="w-full justify-center md:inline-flex space-y-4 md:space-y-0 md:space-x-4 md:p-4"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white"
|
class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white"
|
||||||
|
|
||||||
>
|
>
|
||||||
<div v-if="data.isLive" class="w-full mx-auto rounded-lg mb-5">
|
<div v-if="data.isLive" class="w-full mx-auto rounded-lg mb-5">
|
||||||
<video-player :options="videoOptions"> </video-player>
|
<video-player :options="videoOptions"> </video-player>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-else :src="data.banner" alt="Streamer banner" class="rounded-md opacity-70 relative mb-2">
|
<img
|
||||||
|
v-else
|
||||||
|
:src="data.banner"
|
||||||
|
alt="Streamer banner"
|
||||||
|
class="rounded-md opacity-70 relative mb-2"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="w-full flex-wrap md:p-3">
|
<div class="w-full flex-wrap md:p-3">
|
||||||
<div class="inline-flex md:w-4/5">
|
<div class="inline-flex md:w-4/5">
|
||||||
@ -90,10 +95,10 @@ export default {
|
|||||||
:style="`border-color: ${data.colorHex};`"
|
:style="`border-color: ${data.colorHex};`"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="data.isLive"
|
v-if="data.isLive"
|
||||||
class="absolute flex left-1/2 translate-x-[-50%] whitespace-nowrap uppercase top-16 bg-ctp-red font-bold text-sm p-1.5 py-0.5 rounded-md"
|
class="absolute flex left-1/2 translate-x-[-50%] whitespace-nowrap uppercase top-16 bg-ctp-red font-bold text-sm p-1.5 py-0.5 rounded-md"
|
||||||
>{{ $t("main.live") }}</span
|
>{{ $t('main.live') }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -116,7 +121,9 @@ export default {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
<ul class="text-xs font-bold text-left md:text-right space-x-1 space-y-1 overflow-y-auto">
|
<ul
|
||||||
|
class="text-xs font-bold text-left md:text-right space-x-1 space-y-1 overflow-y-auto"
|
||||||
|
>
|
||||||
<li
|
<li
|
||||||
v-for="tag in data.stream!.tags"
|
v-for="tag in data.stream!.tags"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
@ -130,17 +137,19 @@ export default {
|
|||||||
|
|
||||||
<div class="pt-2 inline-flex">
|
<div class="pt-2 inline-flex">
|
||||||
<follow-button :username="data.username"></follow-button>
|
<follow-button :username="data.username"></follow-button>
|
||||||
<p class="align-baseline font-bold ml-3">{{ abbreviate(data.followers) }} {{ $t("main.followers") }}</p>
|
<p class="align-baseline font-bold ml-3">
|
||||||
|
{{ abbreviate(data.followers) }} {{ $t('main.followers') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VIDEOS TAB -->
|
<!-- VIDEOS TAB -->
|
||||||
<video-tab class="mb-4"></video-tab>
|
<video-tab class="mb-4"></video-tab>
|
||||||
|
|
||||||
<!-- ABOUT TAB -->
|
<!-- ABOUT TAB -->
|
||||||
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
||||||
<div class="inline-flex w-full">
|
<div class="inline-flex w-full">
|
||||||
<span class="pr-3 font-bold text-3xl">{{ $t("streamer.about") }}</span>
|
<span class="pr-3 font-bold text-3xl">{{ $t('streamer.about') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mb-5">{{ data.about }}</p>
|
<p class="mb-5">{{ data.about }}</p>
|
||||||
@ -148,7 +157,7 @@ export default {
|
|||||||
<hr class="my-auto w-full bg-gray-200 rounded-full opacity-40" />
|
<hr class="my-auto w-full bg-gray-200 rounded-full opacity-40" />
|
||||||
|
|
||||||
<ul class="flex font-semibold text-md justify-start flex-wrap flex-row">
|
<ul class="flex font-semibold text-md justify-start flex-wrap flex-row">
|
||||||
<li v-for="link in data.socials">
|
<li v-for="link in data.socials" :key="link.link">
|
||||||
<a :href="link.link" class="text-white hover:text-gray-400 mr-4">
|
<a :href="link.link" class="text-white hover:text-gray-400 mr-4">
|
||||||
<v-icon :name="`bi-${link.type}`" class="w-6 h-6 mr-1"></v-icon>
|
<v-icon :name="`bi-${link.type}`" class="w-6 h-6 mr-1"></v-icon>
|
||||||
<span>{{ link.name }}</span>
|
<span>{{ link.name }}</span>
|
||||||
|
@ -7,23 +7,21 @@ import TwitchChat from '@/components/TwitchChat.vue'
|
|||||||
import ErrorMessage from '@/components/ErrorMessage.vue'
|
import ErrorMessage from '@/components/ErrorMessage.vue'
|
||||||
import FollowButton from '@/components/FollowButton.vue'
|
import FollowButton from '@/components/FollowButton.vue'
|
||||||
import LoadingScreen from '@/components/LoadingScreen.vue'
|
import LoadingScreen from '@/components/LoadingScreen.vue'
|
||||||
import VideoTab from '@/components/user/VideoTab.vue'
|
|
||||||
|
|
||||||
import type { Video, ApiResponse } from '@/types'
|
import type { Video } from '@/types'
|
||||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||||
|
|
||||||
|
|
||||||
interface ChatComponent {
|
interface ChatComponent {
|
||||||
updateVodComments: (time: number) => void
|
updateVodComments: (time: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ["rootBackendUrl"],
|
inject: ['rootBackendUrl'],
|
||||||
async setup() {
|
async setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const vodID = route.params.vodID
|
const vodID = route.params.vodID
|
||||||
const data = ref<Video>()
|
const data = ref<Video>()
|
||||||
const status = ref<"ok" | "error">()
|
const status = ref<'ok' | 'error'>()
|
||||||
const rootBackendUrl = inject('rootBackendUrl')
|
const rootBackendUrl = inject('rootBackendUrl')
|
||||||
const videoOptions = {
|
const videoOptions = {
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
@ -40,30 +38,30 @@ export default {
|
|||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
status,
|
status,
|
||||||
videoOptions,
|
videoOptions
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const vodID = this.$route.params.vodID
|
const vodID = this.$route.params.vodID
|
||||||
|
|
||||||
await getEndpoint("api/vods/" + vodID)
|
await getEndpoint('api/vods/' + vodID)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.data = data
|
this.data = data
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.status = "error"
|
this.status = 'error'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
VideoPlayer,
|
VideoPlayer,
|
||||||
TwitchChat,
|
TwitchChat,
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
FollowButton,
|
FollowButton,
|
||||||
LoadingScreen,
|
LoadingScreen
|
||||||
VideoTab
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
truncate, abbreviate,
|
truncate,
|
||||||
|
abbreviate,
|
||||||
handlePlayerTimeUpdate(time: number) {
|
handlePlayerTimeUpdate(time: number) {
|
||||||
const chat = this.$refs.chat as ChatComponent
|
const chat = this.$refs.chat as ChatComponent
|
||||||
chat.updateVodComments(time)
|
chat.updateVodComments(time)
|
||||||
@ -73,31 +71,35 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||||
<error-message v-else-if="status == 'error'"></error-message>
|
<error-message v-else-if="status == 'error'"></error-message>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="data"
|
v-else-if="data"
|
||||||
class="w-full justify-center md:inline-flex space-y-4 md:space-y-0 md:space-x-4 md:p-4"
|
class="w-full justify-center md:inline-flex space-y-4 md:space-y-0 md:space-x-4 md:p-4"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white"
|
class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white"
|
||||||
|
|
||||||
>
|
>
|
||||||
<div class="w-full mx-auto rounded-lg mb-5">
|
<div class="w-full mx-auto rounded-lg mb-5">
|
||||||
<video-player :options="videoOptions" @PlayerTimeUpdate="handlePlayerTimeUpdate"> </video-player>
|
<video-player :options="videoOptions" @PlayerTimeUpdate="handlePlayerTimeUpdate">
|
||||||
|
</video-player>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex-wrap md:p-3">
|
<div class="w-full flex-wrap md:p-3">
|
||||||
<div class="inline-flex md:w-full">
|
<div class="inline-flex md:w-full">
|
||||||
<img
|
<router-link :to="'/' + data.streamer.login">
|
||||||
:src="data.streamer.pfp"
|
<img
|
||||||
class="rounded-full border-4 p-0.5 w-auto h-20"
|
:src="data.streamer.pfp"
|
||||||
:style="`border-color: ${data.streamer.colorHex};`"
|
class="rounded-full border-4 p-0.5 w-auto h-20"
|
||||||
/>
|
:style="`border-color: ${data.streamer.colorHex};`"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
<div class="ml-3 content-between">
|
<div class="ml-3 content-between">
|
||||||
<h1 class="text-2xl md:text-4xl font-bold">{{ data.streamer.username }}</h1>
|
<router-link :to="'/' + data.streamer.login">
|
||||||
|
<h1 class="text-2xl md:text-4xl font-bold">{{ data.streamer.username }}</h1>
|
||||||
|
</router-link>
|
||||||
<p class="text-sm font-bold text-gray-200 self-end">
|
<p class="text-sm font-bold text-gray-200 self-end">
|
||||||
{{ truncate(data.title, 130) }}
|
{{ truncate(data.title, 130) }}
|
||||||
</p>
|
</p>
|
||||||
@ -106,17 +108,16 @@ export default {
|
|||||||
|
|
||||||
<div class="pt-2 inline-flex">
|
<div class="pt-2 inline-flex">
|
||||||
<follow-button :username="data.streamer.username"></follow-button>
|
<follow-button :username="data.streamer.username"></follow-button>
|
||||||
<p class="align-baseline font-bold ml-3">{{ abbreviate(data.streamer.followers) }} {{ $t("main.followers") }}</p>
|
<p class="align-baseline font-bold ml-3">
|
||||||
|
{{ abbreviate(data.streamer.followers) }} {{ $t('main.followers') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VIDEOS TAB -->
|
|
||||||
<!-- <video-tab class="mb-4"></video-tab> -->
|
|
||||||
|
|
||||||
<!-- ABOUT TAB -->
|
<!-- ABOUT TAB -->
|
||||||
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
||||||
<div class="inline-flex w-full">
|
<div class="inline-flex w-full">
|
||||||
<span class="pr-3 font-bold text-3xl">{{ $t("streamer.about") }}</span>
|
<span class="pr-3 font-bold text-3xl">{{ $t('streamer.about') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mb-5">{{ data.streamer.about }}</p>
|
<p class="mb-5">{{ data.streamer.about }}</p>
|
||||||
@ -124,7 +125,7 @@ export default {
|
|||||||
<hr class="my-auto w-full bg-gray-200 rounded-full opacity-40" />
|
<hr class="my-auto w-full bg-gray-200 rounded-full opacity-40" />
|
||||||
|
|
||||||
<ul class="flex font-semibold text-md justify-start flex-wrap flex-row">
|
<ul class="flex font-semibold text-md justify-start flex-wrap flex-row">
|
||||||
<li v-for="link in data.streamer.socials">
|
<li v-for="link in data.streamer.socials" :key="link.link">
|
||||||
<a :href="link.link" class="text-white hover:text-gray-400 mr-4">
|
<a :href="link.link" class="text-white hover:text-gray-400 mr-4">
|
||||||
<v-icon :name="`bi-${link.type}`" class="w-6 h-6 mr-1"></v-icon>
|
<v-icon :name="`bi-${link.type}`" class="w-6 h-6 mr-1"></v-icon>
|
||||||
<span>{{ link.name }}</span>
|
<span>{{ link.name }}</span>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user