mirror of
https://codeberg.org/SafeTwitch/safetwitch.git
synced 2025-02-03 01:58:48 -05:00
LINT
This commit is contained in:
parent
820ceda499
commit
adcbfcb1be
37 changed files with 601 additions and 554 deletions
|
@ -27,4 +27,4 @@ export const getBadgesFromMessage = (tags: any, allBadges: Badge[]) => {
|
|||
})
|
||||
|
||||
return getBadges(allBadges, formatedBadges)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getBadgesFromMessage } from './badges'
|
|||
export function parseMessage(messageData: any, allBadges: Badge[]): ParsedMessage {
|
||||
const message = JSON.parse(messageData)
|
||||
|
||||
if (message.type === undefined && message.cursor !== "") {
|
||||
if (message.type === undefined && message.cursor !== '') {
|
||||
const data: ParsedMessage = {
|
||||
type: 'PRIVMSG',
|
||||
data: {
|
||||
|
@ -23,9 +23,9 @@ export function parseMessage(messageData: any, allBadges: Badge[]): ParsedMessag
|
|||
case 'PRIVMSG': {
|
||||
const data: ParsedMessage = {
|
||||
type: 'PRIVMSG',
|
||||
data: {
|
||||
message: message.message,
|
||||
username: message.username,
|
||||
data: {
|
||||
message: message.message,
|
||||
username: message.username,
|
||||
color: message.tags.color,
|
||||
badges: getBadgesFromMessage(message.tags, allBadges)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export const createQualitySelector = (player: any) => {
|
|||
const MenuItem = videojs.getComponent('MenuItem')
|
||||
let formatedQualities: { name: string; index: number; id: string }[]
|
||||
|
||||
let t = i18n.global.t
|
||||
const t = i18n.global.t
|
||||
|
||||
const setQuality = (id: string) => {
|
||||
const found = formatedQualities.find((i) => i.id === id)
|
||||
|
@ -56,10 +56,10 @@ export const createQualitySelector = (player: any) => {
|
|||
videojs.registerComponent('CustomMenuButton', CustomMenuButton)
|
||||
const formattedLevels = []
|
||||
|
||||
const updateLevels = (items: { name: string; index: number; id: string; }[]) => {
|
||||
const updateLevels = () => {
|
||||
player.controlBar.removeChild('CustomMenuButton')
|
||||
player.controlBar.addChild('CustomMenuButton', {
|
||||
title: t("player.quality"),
|
||||
title: t('player.quality'),
|
||||
items: formatedQualities
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export * from './Badge'
|
||||
export * from './ParsedMessage'
|
||||
export * from './ParsedMessage'
|
||||
|
|
|
@ -1,48 +1,50 @@
|
|||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
categoryData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
category: props.categoryData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
abbreviate(text: number) {
|
||||
return Intl.NumberFormat('en-US', {
|
||||
//@ts-ignore
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1
|
||||
}).format(text)
|
||||
},
|
||||
props: {
|
||||
categoryData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
category: props.categoryData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
abbreviate(text: number) {
|
||||
return Intl.NumberFormat('en-US', {
|
||||
//@ts-ignore
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1
|
||||
}).format(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-ctp-crust w-40 lg:w-[11rem] md:w-[13.5rem] rounded-lg">
|
||||
<router-link :to="`/game/${category.name}`">
|
||||
<img :src="category.image" class="rounded-lg rounded-b-none w-full" />
|
||||
</router-link>
|
||||
<div class="bg-ctp-crust w-40 lg:w-[11rem] md:w-[13.5rem] rounded-lg">
|
||||
<router-link :to="`/game/${category.name}`">
|
||||
<img :src="category.image" class="rounded-lg rounded-b-none w-full" />
|
||||
</router-link>
|
||||
|
||||
<div class="p-2">
|
||||
<div>
|
||||
<p class="font-bold text-white text-xl sm:text-base md:text-xl">
|
||||
{{ category.displayName }}
|
||||
</p>
|
||||
<p class="text-sm text-white">{{ abbreviate(category.viewers) }} viewers</p>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<div>
|
||||
<p class="font-bold text-white text-xl sm:text-base md:text-xl">
|
||||
{{ category.displayName }}
|
||||
</p>
|
||||
<p class="text-sm text-white">{{ abbreviate(category.viewers) }} viewers</p>
|
||||
</div>
|
||||
|
||||
<ul class="h-8 overflow-hidden">
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul class="h-8 overflow-hidden">
|
||||
<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
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,31 +1,33 @@
|
|||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
channel: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
channelData: props.channel
|
||||
}
|
||||
props: {
|
||||
channel: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
channelData: props.channel
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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="inline-flex space-x-3">
|
||||
<img :src="channelData.pfp" class="rounded-full w-20">
|
||||
<div>
|
||||
<div class="inline-flex w-full justify-between">
|
||||
<h1 class="text-white text-3xl font-bold">{{ channelData.username }}</h1>
|
||||
<p class="text-white float-right ml-5">{{ channelData.followers }} followers</p>
|
||||
</div>
|
||||
<p class="text-white overflow-y-hidden overflow-ellipsis max-h-12">{{ channelData.about }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<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="inline-flex space-x-3">
|
||||
<img :src="channelData.pfp" class="rounded-full w-20" />
|
||||
<div>
|
||||
<div class="inline-flex w-full justify-between">
|
||||
<h1 class="text-white text-3xl font-bold">{{ channelData.username }}</h1>
|
||||
<p class="text-white float-right ml-5">{{ channelData.followers }} followers</p>
|
||||
</div>
|
||||
<p class="text-white overflow-y-hidden overflow-ellipsis max-h-12">
|
||||
{{ channelData.about }}
|
||||
</p>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="rounded-lg z-50 flex m-3 h-20 bg-amber-400 border-4 border-amber-600">
|
||||
<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>
|
||||
</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"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<h1 class="font-bold text-5xl">{{ $t("error.oops") }}</h1>
|
||||
<p class="font-bold text-3xl">{{ $t("error.notsupposedtohappen") }}</p>
|
||||
<h1 class="font-bold text-5xl">{{ $t('error.oops') }}</h1>
|
||||
<p class="font-bold text-3xl">{{ $t('error.notsupposedtohappen') }}</p>
|
||||
</div>
|
||||
|
||||
<p class="text-xl">
|
||||
{{ $t("error.serverexplain") }}
|
||||
{{ $t('error.serverexplain') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -56,7 +56,7 @@ export default {
|
|||
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>
|
||||
<span v-if="isFollowing"> {{ $t("streamer.unfollow") }} </span>
|
||||
<span v-else> {{ $t("streamer.follow") }} </span>
|
||||
<span v-if="isFollowing"> {{ $t('streamer.unfollow') }} </span>
|
||||
<span v-else> {{ $t('streamer.follow') }} </span>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -5,14 +5,14 @@ export default {
|
|||
setup() {
|
||||
const version = inject('version')
|
||||
return {
|
||||
version
|
||||
version
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-2 mt-5 flex justify-center">
|
||||
<p class="text-white font-bold">SafeTwitch v{{ version }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="m-2 mt-5 flex justify-center">
|
||||
<p class="text-white font-bold">SafeTwitch v{{ version }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,32 +1,37 @@
|
|||
<template>
|
||||
<div class="flex">
|
||||
<select v-model="$i18n.locale" @change="onChange()" class="my-auto p-0 pr-9 bg-transparent border-0" :selected="$i18n.locale">
|
||||
<option v-for="(lang, i) in langs" :key="`Lang${i}`" :value="lang">
|
||||
{{ names[i] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<select
|
||||
v-model="$i18n.locale"
|
||||
@change="onChange()"
|
||||
class="my-auto p-0 pr-9 bg-transparent border-0"
|
||||
:selected="$i18n.locale"
|
||||
>
|
||||
<option v-for="(lang, i) in langs" :key="`Lang${i}`" :value="lang">
|
||||
{{ names[i] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
langs: ['en-US', 'es-ES', 'nl-NL', 'pt-PT', 'fa-IR', 'he-IL'],
|
||||
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()
|
||||
}
|
||||
setup() {
|
||||
return {
|
||||
langs: ['en-US', 'es-ES', 'nl-NL', 'pt-PT', 'fa-IR', 'he-IL'],
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,8 +20,8 @@ export default {
|
|||
</div>
|
||||
|
||||
<ul class="inline-flex space-x-6 font-medium">
|
||||
<a href="https://codeberg.org/dragongoose/safetwitch">{{ $t("nav.code") }}</a>
|
||||
<router-link to="/privacy">{{ $t("nav.privacy") }}</router-link>
|
||||
<a href="https://codeberg.org/dragongoose/safetwitch">{{ $t('nav.code') }}</a>
|
||||
<router-link to="/privacy">{{ $t('nav.privacy') }}</router-link>
|
||||
<language-switcher></language-switcher>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -2,30 +2,30 @@
|
|||
export default {
|
||||
setup() {
|
||||
return {
|
||||
searchInput: "",
|
||||
searchInput: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
redirectToSearch() {
|
||||
const query = this.searchInput
|
||||
this.$router.push({ path: '/search/', query: { query }})
|
||||
this.$router.push({ path: '/search/', query: { query } })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative hidden md:block">
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
id="searchBar"
|
||||
name="searchBar"
|
||||
:placeholder="$t('main.search')"
|
||||
v-model="searchInput"
|
||||
@keyup.enter=redirectToSearch
|
||||
class="rounded-md p-1 pl-8 text-black bg-white placeholder:text-black"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="relative hidden md:block">
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
id="searchBar"
|
||||
name="searchBar"
|
||||
:placeholder="$t('main.search')"
|
||||
v-model="searchInput"
|
||||
@keyup.enter="redirectToSearch"
|
||||
class="rounded-md p-1 pl-8 text-black bg-white placeholder:text-black"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { ref, inject, provide } from 'vue'
|
||||
import { ref, inject } from 'vue'
|
||||
|
||||
import BadgeVue from './ChatBadge.vue'
|
||||
import { getBadges } from '@/assets/badges'
|
||||
|
|
|
@ -35,11 +35,10 @@ export default {
|
|||
const emit = this.$emit
|
||||
this.player = videojs('video-player', this.options, () => {
|
||||
createQualitySelector(this.player)
|
||||
let i = 0
|
||||
|
||||
this.player.on('timeupdate', () => {
|
||||
emit('PlayerTimeUpdate', this.player.currentTime())
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
unmounted() {
|
||||
|
|
|
@ -1,49 +1,54 @@
|
|||
<template>
|
||||
<div class="min-w-[300px]">
|
||||
<div class="relative">
|
||||
<RouterLink :to="'/videos/' + data.id">
|
||||
<img :src="data.preview" class="rounded-md" width="300">
|
||||
</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 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>
|
||||
</div>
|
||||
|
||||
<div class="pt-2 space-x-2">
|
||||
<div class="space-x-2 inline-flex">
|
||||
<RouterLink :to="'/game/' + data.game.name">
|
||||
<img :src="data.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>{{ data.streamer.login }}</p>
|
||||
<p>{{ data.game.displayName || data.game.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-[300px]">
|
||||
<div class="relative">
|
||||
<RouterLink :to="'/videos/' + videoData.id">
|
||||
<img :src="videoData.preview" class="rounded-md" width="300" />
|
||||
</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(videoData.duration * 1000).toISOString().slice(11, 19) }}
|
||||
</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(videoData.views) }} {{ $t('main.views') }}
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Video } from '@/types/VOD';
|
||||
import { abbreviate } from '@/mixins';
|
||||
import type { Video } from '@/types/VOD'
|
||||
import { abbreviate } from '@/mixins'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
data: Object
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
data: props.data as Video
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
abbreviate
|
||||
props: {
|
||||
data: Object
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
videoData: props.data as Video
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
abbreviate
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -3,26 +3,32 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
||||
<div class="inline-flex w-full">
|
||||
<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 class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
||||
<div class="inline-flex w-full">
|
||||
<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" :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>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -33,25 +39,25 @@ import type { Shelve } from '@/types/VOD'
|
|||
import VideoPreview from '@/components/user/VideoPreview.vue'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
shelves: ref<Shelve[]>([]),
|
||||
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
|
||||
setup() {
|
||||
return {
|
||||
shelves: ref<Shelve[]>([]),
|
||||
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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -17,6 +17,6 @@ export default createI18n({
|
|||
'nl-NL': nl,
|
||||
'pt-PT': pt,
|
||||
'fa-IR': fa,
|
||||
'he-IL': he,
|
||||
'he-IL': he
|
||||
}
|
||||
})
|
||||
|
|
|
@ -2,9 +2,8 @@ import { createApp } from 'vue'
|
|||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/index.css'
|
||||
import i18n from "./i18n"
|
||||
import { version } from "../package.json"
|
||||
|
||||
import i18n from './i18n'
|
||||
import { version } from '../package.json'
|
||||
|
||||
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"
|
||||
// returns false, even if it is true.
|
||||
// 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 wsProtocol = https ? 'wss://' : 'ws://'
|
||||
|
@ -51,4 +50,4 @@ addIcons(
|
|||
|
||||
app.component('v-icon', OhVueIcon)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
app.mount('#app')
|
||||
|
|
|
@ -1,48 +1,42 @@
|
|||
export function truncate(value: string, length: number) {
|
||||
if (value.length > length) {
|
||||
return value.substring(0, length) + '...'
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
if (value.length > length) {
|
||||
return value.substring(0, length) + '...'
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
let language = localStorage.getItem("language") || "en-us"
|
||||
const language = localStorage.getItem('language') || 'en-us'
|
||||
|
||||
export function abbreviate(text: number) {
|
||||
return Intl.NumberFormat(language, {
|
||||
//@ts-ignore
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1
|
||||
}).format(text)
|
||||
return Intl.NumberFormat(language, {
|
||||
//@ts-ignore
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1
|
||||
}).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 rootBackendUrl = `${protocol}${import.meta.env.SAFETWITCH_BACKEND_DOMAIN}/`
|
||||
|
||||
export async function getEndpoint(endpoint: string) {
|
||||
let data
|
||||
|
||||
try {
|
||||
const res = await fetch(rootBackendUrl + endpoint, {
|
||||
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 res = await fetch(rootBackendUrl + endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept-Language': language
|
||||
}
|
||||
})
|
||||
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 {
|
||||
status: "ok" | "error",
|
||||
data: any
|
||||
}
|
||||
status: 'ok' | 'error'
|
||||
data: any
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
export type Tag = string
|
||||
|
||||
export interface CategoryPreview {
|
||||
name: string
|
||||
displayName: string
|
||||
viewers: number
|
||||
tags: Tag[]
|
||||
createdAt?: Date
|
||||
cursor?: string
|
||||
image: string
|
||||
}
|
||||
name: string
|
||||
displayName: string
|
||||
viewers: number
|
||||
tags: Tag[]
|
||||
createdAt?: Date
|
||||
cursor?: string
|
||||
image: string
|
||||
}
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import type { Tag } from "./"
|
||||
import type { StreamData } from "./"
|
||||
import type { Tag } from './'
|
||||
|
||||
export interface CategoryMinifiedStream {
|
||||
title: string
|
||||
viewers: number
|
||||
preview: string
|
||||
tags: Tag[]
|
||||
cursor: string
|
||||
streamer: {
|
||||
name: string
|
||||
pfp: string
|
||||
colorHex: string
|
||||
}
|
||||
title: string
|
||||
viewers: number
|
||||
preview: string
|
||||
tags: Tag[]
|
||||
cursor: string
|
||||
streamer: {
|
||||
name: string
|
||||
pfp: string
|
||||
colorHex: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CategoryData {
|
||||
name: string
|
||||
cover: string
|
||||
description: string
|
||||
viewers: number
|
||||
followers: number
|
||||
tags: Tag[]
|
||||
streams: CategoryMinifiedStream[]
|
||||
}
|
||||
name: string
|
||||
cover: string
|
||||
description: string
|
||||
viewers: number
|
||||
followers: number
|
||||
tags: Tag[]
|
||||
streams: CategoryMinifiedStream[]
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
export interface TwitchChatOptions {
|
||||
login: {
|
||||
username: string,
|
||||
password: string
|
||||
},
|
||||
channels: string[]
|
||||
login: {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
channels: string[]
|
||||
}
|
||||
|
||||
export const MessageTypes = ['PRIVMSG', 'WHISPER']
|
||||
export type MessageType = typeof MessageTypes[number];
|
||||
export type MessageType = (typeof MessageTypes)[number]
|
||||
|
||||
export interface Metadata {
|
||||
username: string
|
||||
messageType: MessageType
|
||||
channel: string
|
||||
message: string
|
||||
tags: { [k:string]:any }
|
||||
username: string
|
||||
messageType: MessageType
|
||||
channel: string
|
||||
message: string
|
||||
tags: { [k: string]: any }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export interface Emote {
|
||||
name: string,
|
||||
urls : {
|
||||
[k: string]: string
|
||||
}
|
||||
}
|
||||
name: string
|
||||
urls: {
|
||||
[k: string]: string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { StreamData, StreamerData } from "./"
|
||||
import type { CategoryPreview } from "./"
|
||||
import type { StreamerData } from './'
|
||||
import type { CategoryPreview } from './'
|
||||
|
||||
export interface SearchResult {
|
||||
channels: StreamerData[]
|
||||
categories: CategoryPreview[]
|
||||
relatedChannels: StreamerData[]
|
||||
channelsWithTag: StreamerData[]
|
||||
}
|
||||
channels: StreamerData[]
|
||||
categories: CategoryPreview[]
|
||||
relatedChannels: StreamerData[]
|
||||
channelsWithTag: StreamerData[]
|
||||
}
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
export interface Social {
|
||||
type: string | null
|
||||
name: string,
|
||||
link: string
|
||||
type: string | null
|
||||
name: string
|
||||
link: string
|
||||
}
|
||||
|
||||
export interface StreamData {
|
||||
tags: string[]
|
||||
title: string
|
||||
topic: string
|
||||
startedAt: number
|
||||
viewers: number
|
||||
preview: string
|
||||
cursor?: string
|
||||
tags: string[]
|
||||
title: string
|
||||
topic: string
|
||||
startedAt: number
|
||||
viewers: number
|
||||
preview: string
|
||||
cursor?: string
|
||||
}
|
||||
|
||||
export interface StreamerData {
|
||||
username: string
|
||||
login: string
|
||||
followers: number
|
||||
isLive: boolean
|
||||
about: string
|
||||
socials?: Social[]
|
||||
pfp: string
|
||||
banner: string
|
||||
stream?: StreamData | null
|
||||
isPartner: boolean | null
|
||||
colorHex: string
|
||||
id: number
|
||||
}
|
||||
username: string
|
||||
login: string
|
||||
followers: number
|
||||
isLive: boolean
|
||||
about: string
|
||||
socials?: Social[]
|
||||
pfp: string
|
||||
banner: string
|
||||
stream?: StreamData | null
|
||||
isPartner: boolean | null
|
||||
colorHex: string
|
||||
id: number
|
||||
}
|
||||
|
|
|
@ -1,51 +1,49 @@
|
|||
import type { StreamerData } from "./Streamer"
|
||||
import type { StreamerData } from './Streamer'
|
||||
|
||||
export interface MinifiedCategory {
|
||||
image: string
|
||||
id: string
|
||||
name: string
|
||||
displayName: string
|
||||
export interface MinifiedCategory {
|
||||
image: string
|
||||
id: string
|
||||
name: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export interface MinifiedStreamer {
|
||||
name: string
|
||||
login: string
|
||||
pfp: string
|
||||
colorHex: string
|
||||
export interface MinifiedStreamer {
|
||||
name: string
|
||||
login: string
|
||||
pfp: string
|
||||
colorHex: string
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
preview: string
|
||||
game: MinifiedCategory
|
||||
duration: number
|
||||
title: string
|
||||
publishedAt: string
|
||||
views: number
|
||||
tag: string[]
|
||||
streamer: StreamerData
|
||||
export interface Video {
|
||||
preview: string
|
||||
game: MinifiedCategory
|
||||
duration: number
|
||||
title: string
|
||||
publishedAt: string
|
||||
views: number
|
||||
tag: string[]
|
||||
streamer: StreamerData
|
||||
}
|
||||
|
||||
export interface Shelve {
|
||||
title: string
|
||||
videos: Video[]
|
||||
export interface Shelve {
|
||||
title: string
|
||||
videos: Video[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface VodMessager {
|
||||
name: string
|
||||
login: string
|
||||
name: string
|
||||
login: string
|
||||
}
|
||||
|
||||
export interface VodCommentBadge {
|
||||
version: number
|
||||
setId: string
|
||||
version: number
|
||||
setId: string
|
||||
}
|
||||
|
||||
export interface VodComment {
|
||||
message: string
|
||||
messager: MinifiedStreamer
|
||||
offset: number
|
||||
cursor: string
|
||||
badges: VodCommentBadge[]
|
||||
}
|
||||
message: string
|
||||
messager: MinifiedStreamer
|
||||
offset: number
|
||||
cursor: string
|
||||
badges: VodCommentBadge[]
|
||||
}
|
||||
|
|
|
@ -5,4 +5,4 @@ export * from './Chat'
|
|||
export * from './Category'
|
||||
export * from './CategoryData'
|
||||
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 { getEndpoint, abbreviate } from '@/mixins'
|
||||
|
||||
|
||||
export default {
|
||||
inject: ['protocol'],
|
||||
async setup() {
|
||||
let data = ref<CategoryData>()
|
||||
let status = ref<"ok" | "error">()
|
||||
let status = ref<'ok' | 'error'>()
|
||||
|
||||
return {
|
||||
data,
|
||||
|
@ -21,13 +20,13 @@ export default {
|
|||
}
|
||||
},
|
||||
async mounted() {
|
||||
await getEndpoint("api/discover/" + this.$route.params.game)
|
||||
.catch(() => {
|
||||
this.status = "error"
|
||||
})
|
||||
.then((data: CategoryData) => {
|
||||
this.data = data
|
||||
})
|
||||
await getEndpoint('api/discover/' + this.$route.params.game)
|
||||
.catch(() => {
|
||||
this.status = 'error'
|
||||
})
|
||||
.then((data: CategoryData) => {
|
||||
this.data = data
|
||||
})
|
||||
|
||||
this.getMoreStreams()
|
||||
},
|
||||
|
@ -45,10 +44,11 @@ export default {
|
|||
if (!cursor) return
|
||||
|
||||
// get rest of streams from api
|
||||
const resData = await getEndpoint(`api/discover/${this.$route.params.game}/?cursor=${cursor}`)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const resData = await getEndpoint(
|
||||
`api/discover/${this.$route.params.game}/?cursor=${cursor}`
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
for (let stream of resData.streams) {
|
||||
this.data!.streams.push(stream)
|
||||
|
@ -56,7 +56,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
abbreviate,
|
||||
abbreviate
|
||||
},
|
||||
components: {
|
||||
StreamPreviewVue,
|
||||
|
@ -80,35 +80,47 @@ export default {
|
|||
<div class="hidden md:block">
|
||||
<div>
|
||||
<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">{{ $t("main.viewers") }}: {{ abbreviate(data.viewers) }}</p>
|
||||
<p class="font-bold text-white text-lg">
|
||||
{{ $t('main.followers') }}: {{ abbreviate(data.followers) }}
|
||||
</p>
|
||||
<p class="font-bold text-white text-lg">
|
||||
{{ $t('main.viewers') }}: {{ abbreviate(data.viewers) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul class="mb-5">
|
||||
<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">{{
|
||||
tag
|
||||
}}</span>
|
||||
<span
|
||||
class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-ctp-overlay1 rounded-sm"
|
||||
>{{ tag }}</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</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 class="md:hidden">
|
||||
<div>
|
||||
<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">{{ $t("main.viewers") }}: {{ abbreviate(data.viewers) }}</p>
|
||||
<p class="font-bold text-white text-lg">
|
||||
{{ $t('main.followers') }}: {{ abbreviate(data.followers) }}
|
||||
</p>
|
||||
<p class="font-bold text-white text-lg">
|
||||
{{ $t('main.viewers') }}: {{ abbreviate(data.viewers) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul class="mb-5">
|
||||
<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">{{
|
||||
tag
|
||||
}}</span>
|
||||
<span
|
||||
class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-ctp-overlay1 rounded-sm"
|
||||
>{{ tag }}</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -118,7 +130,11 @@ export default {
|
|||
|
||||
<div class="max-w-[58rem] mx-auto">
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -13,7 +13,7 @@ export default {
|
|||
inject: ['protocol'],
|
||||
async setup() {
|
||||
let data = ref<CategoryPreviewInterface[]>()
|
||||
let status = ref<"ok" | "error">()
|
||||
let status = ref<'ok' | 'error'>()
|
||||
|
||||
return {
|
||||
data,
|
||||
|
@ -57,7 +57,9 @@ export default {
|
|||
const cursor = this.data[this.data.length - 1].cursor
|
||||
if (!cursor) return
|
||||
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) {
|
||||
throw new Error('Failed to fetch data')
|
||||
|
@ -81,14 +83,13 @@ export default {
|
|||
this.following = []
|
||||
}
|
||||
|
||||
await getEndpoint("api/discover")
|
||||
.catch(() => {
|
||||
this.status = "error"
|
||||
})
|
||||
.then((data: CategoryPreviewInterface[]) => {
|
||||
this.data = data
|
||||
})
|
||||
|
||||
await getEndpoint('api/discover')
|
||||
.catch(() => {
|
||||
this.status = 'error'
|
||||
})
|
||||
.then((data: CategoryPreviewInterface[]) => {
|
||||
this.data = data
|
||||
})
|
||||
},
|
||||
components: {
|
||||
StreamPreviewVue,
|
||||
|
@ -119,13 +120,13 @@ export default {
|
|||
</div>
|
||||
|
||||
<div class="p-2">
|
||||
<h1 class="font-bold text-5xl text-white">{{ $t("home.discover") }}</h1>
|
||||
<p class="text-xl text-white">{{ $t("home.discoverDescription") }}</p>
|
||||
<h1 class="font-bold text-5xl text-white">{{ $t('home.discover') }}</h1>
|
||||
<p class="text-xl text-white">{{ $t('home.discoverDescription') }}</p>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -143,10 +144,11 @@ export default {
|
|||
<ul ref="categoryList">
|
||||
<li
|
||||
v-for="category in data"
|
||||
:key="category.name"
|
||||
ref="categoryItem"
|
||||
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>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -4,8 +4,8 @@ export default {}
|
|||
|
||||
<template>
|
||||
<div class="flex flex-col items-center pt-10 font-bold text-5xl text-white">
|
||||
<h1>{{ $t("error.oops") }}</h1>
|
||||
<h1>{{ $t("error.notfound") }}</h1>
|
||||
<h1>{{ $t('error.oops') }}</h1>
|
||||
<h1>{{ $t('error.notfound') }}</h1>
|
||||
<h2 class="text-4xl">maybe go <RouterLink to="/" class="text-gray-500">home</RouterLink>?</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -3,22 +3,22 @@ export default {}
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="prose prose-invert border-2 bg-ctp-crust rounded-lg mx-auto p-8 pt-10 text-white"
|
||||
>
|
||||
<article class="prose prose-invert border-2 bg-ctp-crust rounded-lg mx-auto p-8 pt-10 text-white">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>
|
||||
It's.... kind of empty here.
|
||||
|
||||
<br><br>
|
||||
|
||||
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>
|
||||
<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>
|
||||
</article>
|
||||
</template>
|
||||
|
|
|
@ -8,90 +8,102 @@ import StreamPreviewVue from '@/components/StreamPreview.vue'
|
|||
import ChannelPreview from '@/components/ChannelPreview.vue'
|
||||
|
||||
import { getEndpoint } from '@/mixins'
|
||||
import type { ApiResponse, SearchResult, StreamerData } from '@/types'
|
||||
import type { SearchResult, StreamerData } from '@/types'
|
||||
|
||||
export default {
|
||||
inject: ['protocol'],
|
||||
setup() {
|
||||
let data = ref<SearchResult>()
|
||||
const status = ref<"ok" | "error">()
|
||||
return {
|
||||
data,
|
||||
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
|
||||
inject: ['protocol'],
|
||||
setup() {
|
||||
let data = ref<SearchResult>()
|
||||
const status = ref<'ok' | 'error'>()
|
||||
return {
|
||||
data,
|
||||
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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||
<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>
|
||||
|
||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||
<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"
|
||||
:key="channel.id"
|
||||
class="m-2 hover:scale-105 transition-transform"
|
||||
>
|
||||
<channel-preview :channel="channel"></channel-preview>
|
||||
</li>
|
||||
</ul>
|
||||
</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 VideoTab from '@/components/user/VideoTab.vue'
|
||||
|
||||
import type { StreamerData, ApiResponse } from '@/types'
|
||||
import type { StreamerData } from '@/types'
|
||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||
|
||||
export default {
|
||||
inject: ["rootBackendUrl"],
|
||||
inject: ['rootBackendUrl'],
|
||||
async setup() {
|
||||
const route = useRoute()
|
||||
const username = route.params.username
|
||||
const data = ref<StreamerData>()
|
||||
const status = ref<"ok" | "error">()
|
||||
const status = ref<'ok' | 'error'>()
|
||||
const rootBackendUrl = inject('rootBackendUrl')
|
||||
const videoOptions = {
|
||||
autoplay: true,
|
||||
|
@ -35,19 +35,19 @@ export default {
|
|||
return {
|
||||
data,
|
||||
status,
|
||||
videoOptions,
|
||||
videoOptions
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const username = this.$route.params.username
|
||||
|
||||
await getEndpoint("api/users/" + username)
|
||||
.then((data) => {
|
||||
this.data = data
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = "error"
|
||||
})
|
||||
await getEndpoint('api/users/' + username)
|
||||
.then((data) => {
|
||||
this.data = data
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = 'error'
|
||||
})
|
||||
},
|
||||
components: {
|
||||
VideoPlayer,
|
||||
|
@ -58,28 +58,33 @@ export default {
|
|||
VideoTab
|
||||
},
|
||||
methods: {
|
||||
truncate, abbreviate
|
||||
truncate,
|
||||
abbreviate
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||
<error-message v-else-if="status == 'error'"></error-message>
|
||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||
<error-message v-else-if="status == 'error'"></error-message>
|
||||
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<video-player :options="videoOptions"> </video-player>
|
||||
</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="inline-flex md:w-4/5">
|
||||
|
@ -90,10 +95,10 @@ export default {
|
|||
:style="`border-color: ${data.colorHex};`"
|
||||
/>
|
||||
|
||||
<span
|
||||
<span
|
||||
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"
|
||||
>{{ $t("main.live") }}</span
|
||||
>{{ $t('main.live') }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
|
@ -116,7 +121,9 @@ export default {
|
|||
</p>
|
||||
</div>
|
||||
<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
|
||||
v-for="tag in data.stream!.tags"
|
||||
:key="tag"
|
||||
|
@ -130,17 +137,19 @@ export default {
|
|||
|
||||
<div class="pt-2 inline-flex">
|
||||
<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>
|
||||
|
||||
<!-- VIDEOS TAB -->
|
||||
<video-tab class="mb-4"></video-tab>
|
||||
<video-tab class="mb-4"></video-tab>
|
||||
|
||||
<!-- ABOUT TAB -->
|
||||
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
||||
<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>
|
||||
|
||||
<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" />
|
||||
|
||||
<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">
|
||||
<v-icon :name="`bi-${link.type}`" class="w-6 h-6 mr-1"></v-icon>
|
||||
<span>{{ link.name }}</span>
|
||||
|
|
|
@ -7,23 +7,21 @@ import TwitchChat from '@/components/TwitchChat.vue'
|
|||
import ErrorMessage from '@/components/ErrorMessage.vue'
|
||||
import FollowButton from '@/components/FollowButton.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'
|
||||
|
||||
|
||||
interface ChatComponent {
|
||||
updateVodComments: (time: number) => void
|
||||
}
|
||||
|
||||
export default {
|
||||
inject: ["rootBackendUrl"],
|
||||
inject: ['rootBackendUrl'],
|
||||
async setup() {
|
||||
const route = useRoute()
|
||||
const vodID = route.params.vodID
|
||||
const data = ref<Video>()
|
||||
const status = ref<"ok" | "error">()
|
||||
const status = ref<'ok' | 'error'>()
|
||||
const rootBackendUrl = inject('rootBackendUrl')
|
||||
const videoOptions = {
|
||||
autoplay: true,
|
||||
|
@ -40,30 +38,30 @@ export default {
|
|||
return {
|
||||
data,
|
||||
status,
|
||||
videoOptions,
|
||||
videoOptions
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const vodID = this.$route.params.vodID
|
||||
|
||||
await getEndpoint("api/vods/" + vodID)
|
||||
.then((data) => {
|
||||
this.data = data
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = "error"
|
||||
})
|
||||
await getEndpoint('api/vods/' + vodID)
|
||||
.then((data) => {
|
||||
this.data = data
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = 'error'
|
||||
})
|
||||
},
|
||||
components: {
|
||||
VideoPlayer,
|
||||
TwitchChat,
|
||||
ErrorMessage,
|
||||
FollowButton,
|
||||
LoadingScreen,
|
||||
VideoTab
|
||||
LoadingScreen
|
||||
},
|
||||
methods: {
|
||||
truncate, abbreviate,
|
||||
truncate,
|
||||
abbreviate,
|
||||
handlePlayerTimeUpdate(time: number) {
|
||||
const chat = this.$refs.chat as ChatComponent
|
||||
chat.updateVodComments(time)
|
||||
|
@ -73,31 +71,35 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||
<error-message v-else-if="status == 'error'"></error-message>
|
||||
<loading-screen v-if="!data && status != 'error'"></loading-screen>
|
||||
<error-message v-else-if="status == 'error'"></error-message>
|
||||
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<video-player :options="videoOptions" @PlayerTimeUpdate="handlePlayerTimeUpdate"> </video-player>
|
||||
<video-player :options="videoOptions" @PlayerTimeUpdate="handlePlayerTimeUpdate">
|
||||
</video-player>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex-wrap md:p-3">
|
||||
<div class="inline-flex md:w-full">
|
||||
<img
|
||||
:src="data.streamer.pfp"
|
||||
class="rounded-full border-4 p-0.5 w-auto h-20"
|
||||
:style="`border-color: ${data.streamer.colorHex};`"
|
||||
/>
|
||||
|
||||
<router-link :to="'/' + data.streamer.login">
|
||||
<img
|
||||
:src="data.streamer.pfp"
|
||||
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">
|
||||
<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">
|
||||
{{ truncate(data.title, 130) }}
|
||||
</p>
|
||||
|
@ -106,17 +108,16 @@ export default {
|
|||
|
||||
<div class="pt-2 inline-flex">
|
||||
<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>
|
||||
|
||||
<!-- VIDEOS TAB -->
|
||||
<!-- <video-tab class="mb-4"></video-tab> -->
|
||||
|
||||
<!-- ABOUT TAB -->
|
||||
<div class="bg-ctp-mantle mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
||||
<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>
|
||||
|
||||
<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" />
|
||||
|
||||
<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">
|
||||
<v-icon :name="`bi-${link.type}`" class="w-6 h-6 mr-1"></v-icon>
|
||||
<span>{{ link.name }}</span>
|
||||
|
|
Loading…
Add table
Reference in a new issue