Format
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<audio ref="videoPlayer" class="w-full" controls></audio>
|
||||
</div>
|
||||
<div>
|
||||
<audio ref="videoPlayer" class="w-full" controls></audio>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// Importing video-js
|
||||
@ -9,54 +9,53 @@ import Hls from 'hls.js'
|
||||
import { getSetting } from '@/settingsManager'
|
||||
|
||||
export default {
|
||||
name: 'VideoJsPlayer',
|
||||
props: {
|
||||
masterManifestUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
emits: ['PlayerTimeUpdate'],
|
||||
async setup(props) {
|
||||
let player: any
|
||||
|
||||
const getAudioOnlyManifestFromUrl = async (masterManifestUrl: string) => {
|
||||
const manifestRes = await fetch(masterManifestUrl)
|
||||
if (!manifestRes.ok) return;
|
||||
|
||||
const manifest = await manifestRes.text()
|
||||
// The last line of the manifest is the
|
||||
// audio only manifest. This is a bit hacky
|
||||
// but it'll work. If issues arise we can
|
||||
// always implement an actual m3u8 parser
|
||||
const tmp = manifest.split("\n")
|
||||
const audioOnlyManifest = tmp[tmp.length - 1]
|
||||
|
||||
return audioOnlyManifest
|
||||
}
|
||||
|
||||
const audioOnlyManifest = await getAudioOnlyManifestFromUrl(props.masterManifestUrl)
|
||||
return {
|
||||
player,
|
||||
audioOnlyManifest
|
||||
}
|
||||
},
|
||||
// initializing the video player
|
||||
// when the component is being mounted
|
||||
mounted() {
|
||||
const video = this.$refs.videoPlayer as HTMLVideoElement;
|
||||
if (Hls.isSupported()) {
|
||||
const hls = new Hls();
|
||||
|
||||
hls.loadSource(this.audioOnlyManifest || "");
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
if(getSetting("autoplay")) {
|
||||
video.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
name: 'VideoJsPlayer',
|
||||
props: {
|
||||
masterManifestUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['PlayerTimeUpdate'],
|
||||
async setup(props) {
|
||||
let player: any
|
||||
|
||||
const getAudioOnlyManifestFromUrl = async (masterManifestUrl: string) => {
|
||||
const manifestRes = await fetch(masterManifestUrl)
|
||||
if (!manifestRes.ok) return
|
||||
|
||||
const manifest = await manifestRes.text()
|
||||
// The last line of the manifest is the
|
||||
// audio only manifest. This is a bit hacky
|
||||
// but it'll work. If issues arise we can
|
||||
// always implement an actual m3u8 parser
|
||||
const tmp = manifest.split('\n')
|
||||
const audioOnlyManifest = tmp[tmp.length - 1]
|
||||
|
||||
return audioOnlyManifest
|
||||
}
|
||||
|
||||
const audioOnlyManifest = await getAudioOnlyManifestFromUrl(props.masterManifestUrl)
|
||||
return {
|
||||
player,
|
||||
audioOnlyManifest
|
||||
}
|
||||
},
|
||||
// initializing the video player
|
||||
// when the component is being mounted
|
||||
mounted() {
|
||||
const video = this.$refs.videoPlayer as HTMLVideoElement
|
||||
if (Hls.isSupported()) {
|
||||
const hls = new Hls()
|
||||
|
||||
hls.loadSource(this.audioOnlyManifest || '')
|
||||
hls.attachMedia(video)
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
if (getSetting('autoplay')) {
|
||||
video.play()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -39,10 +39,9 @@ export default {
|
||||
|
||||
<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-surface0 rounded-md m-0.5 text-xs font-bold text-contrast"
|
||||
>{{ tag }}</span
|
||||
>
|
||||
<span class="p-2.5 py-1.5 bg-surface0 rounded-md m-0.5 text-xs font-bold text-contrast">{{
|
||||
tag
|
||||
}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@ export default {
|
||||
methods: {
|
||||
followStreamer() {
|
||||
const username = this.$props.username
|
||||
const follows = localStorage.getItem('following') || "[]"
|
||||
const follows = localStorage.getItem('following') || '[]'
|
||||
|
||||
let parsedFollows: string[] = JSON.parse(follows)
|
||||
|
||||
|
@ -4,12 +4,12 @@ export default {
|
||||
return {
|
||||
version: `${import.meta.env.SAFETWITCH_TAG}-${import.meta.env.SAFETWITCH_COMMIT_HASH}`
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-2 mt-5 flex justify-center">
|
||||
<p class="text-contrast font-bold">SafeTwitch {{ version}}</p>
|
||||
<p class="text-contrast font-bold">SafeTwitch {{ version }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -17,7 +17,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
const savedLocale = localStorage.getItem('language')
|
||||
if(savedLocale) {
|
||||
if (savedLocale) {
|
||||
this.$i18n.locale = savedLocale
|
||||
}
|
||||
}
|
||||
@ -33,18 +33,12 @@ export default {
|
||||
</div>
|
||||
|
||||
<search-bar class="mt-4 mr-4 hidden sm:inline-block sm:mt-0"></search-bar>
|
||||
|
||||
|
||||
<div class="text-contrast hidden space-x-4 sm:block">
|
||||
<a
|
||||
href="https://codeberg.org/dragongoose/safetwitch"
|
||||
target="_blank"
|
||||
>{{ $t('nav.code') }}</a
|
||||
>
|
||||
<a href="https://codeberg.org/dragongoose/safetwitch" target="_blank">{{ $t('nav.code') }}</a>
|
||||
<a :href="'https://twitch.tv' + $route.fullPath">Twitch</a>
|
||||
<router-link to="/privacy">{{
|
||||
$t('nav.privacy')
|
||||
}}</router-link>
|
||||
<router-link to="/settings">{{ $t("nav.settings") }}</router-link>
|
||||
<router-link to="/privacy">{{ $t('nav.privacy') }}</router-link>
|
||||
<router-link to="/settings">{{ $t('nav.settings') }}</router-link>
|
||||
</div>
|
||||
|
||||
<div class="block sm:hidden">
|
||||
@ -63,7 +57,7 @@ export default {
|
||||
<a href="https://codeberg.org/dragongoose/safetwitch">{{ $t('nav.code') }}</a>
|
||||
<a :href="'https://twitch.tv' + $route.fullPath">Twitch</a>
|
||||
<router-link to="/privacy">{{ $t('nav.privacy') }}</router-link>
|
||||
<router-link to="/settings">{{ $t("nav.settings") }}</router-link>
|
||||
<router-link to="/settings">{{ $t('nav.settings') }}</router-link>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -24,9 +24,9 @@ export default {
|
||||
login: streamData.streamer.name,
|
||||
followers: 0,
|
||||
isLive: true,
|
||||
about: "",
|
||||
about: '',
|
||||
pfp: streamData.streamer.pfp,
|
||||
banner: "",
|
||||
banner: '',
|
||||
isPartner: false,
|
||||
colorHex: streamData.streamer.colorHex,
|
||||
id: 0,
|
||||
@ -34,13 +34,13 @@ export default {
|
||||
title: streamData.title,
|
||||
tags: streamData.tags,
|
||||
startedAt: 0,
|
||||
topic: "",
|
||||
topic: '',
|
||||
viewers: streamData.viewers,
|
||||
preview: streamData.preview
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = await getEndpoint("api/users/" + props.name)
|
||||
data = await getEndpoint('api/users/' + props.name)
|
||||
}
|
||||
|
||||
const frontend_url = protocol + import.meta.env.SAFETWITCH_INSTANCE_DOMAIN
|
||||
@ -68,7 +68,9 @@ export default {
|
||||
<div class="inline-flex">
|
||||
<img :src="data.pfp" class="rounded-full mr-2" />
|
||||
<div class="w-full">
|
||||
<p class="font-bold w-[18rem] md:w-[22.9rem] truncate" :title="data.stream.title">{{ data.stream.title }}</p>
|
||||
<p class="font-bold w-[18rem] md:w-[22.9rem] truncate" :title="data.stream.title">
|
||||
{{ data.stream.title }}
|
||||
</p>
|
||||
<div class="inline-flex w-full justify-between">
|
||||
<p class="text-neutral">{{ data.username }}</p>
|
||||
<p class="self-end float-right">
|
||||
|
@ -37,7 +37,7 @@ export default {
|
||||
createQualitySelector(this.player)
|
||||
|
||||
if (this.$route.query['t']) {
|
||||
const timeQuery = this.$route.query['t'].toString() || ""
|
||||
const timeQuery = this.$route.query['t'].toString() || ''
|
||||
const time = getTimeFromQuery(timeQuery)
|
||||
this.player.currentTime(time)
|
||||
}
|
||||
|
@ -1,108 +1,129 @@
|
||||
<script lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
import { ref } from "vue";
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const instanceUrl = location.protocol + '//' + location.host;
|
||||
let currentUrl = ref(instanceUrl)
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const instanceUrl = location.protocol + '//' + location.host
|
||||
let currentUrl = ref(instanceUrl)
|
||||
|
||||
return {
|
||||
// remove any query from the path
|
||||
path: route.fullPath.split("?")[0],
|
||||
usingTwitchUrl: ref(false),
|
||||
usingTime: ref(false),
|
||||
query: ref(""),
|
||||
instanceUrl,
|
||||
currentUrl
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
props: {
|
||||
time: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
useTime: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatSecondsToQuery(sec: number) {
|
||||
const date = new Date(sec * 1000)
|
||||
const hour = date.getHours()
|
||||
const minutes = date.getMinutes()
|
||||
const seconds = date.getSeconds()
|
||||
|
||||
return `${hour}h${minutes}m${seconds}s`
|
||||
},
|
||||
toggleTwitchUrl() {
|
||||
if (this.usingTwitchUrl) {
|
||||
this.currentUrl = "https://twitch.tv"
|
||||
} else {
|
||||
this.currentUrl = this.instanceUrl
|
||||
}
|
||||
},
|
||||
async copyUrl() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.currentUrl + this.path + this.query);
|
||||
console.log('Content copied to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
},
|
||||
toggleTime() {
|
||||
if (this.usingTime) {
|
||||
const timestamp = this.formatSecondsToQuery(this.$props.time)
|
||||
this.query = "?t=" + timestamp
|
||||
} else {
|
||||
this.query = ""
|
||||
}
|
||||
},
|
||||
gotoUrl() {
|
||||
location.href = this.currentUrl + this.path + this.query
|
||||
}
|
||||
return {
|
||||
// remove any query from the path
|
||||
path: route.fullPath.split('?')[0],
|
||||
usingTwitchUrl: ref(false),
|
||||
usingTime: ref(false),
|
||||
query: ref(''),
|
||||
instanceUrl,
|
||||
currentUrl
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
props: {
|
||||
time: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
useTime: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatSecondsToQuery(sec: number) {
|
||||
const date = new Date(sec * 1000)
|
||||
const hour = date.getHours()
|
||||
const minutes = date.getMinutes()
|
||||
const seconds = date.getSeconds()
|
||||
|
||||
return `${hour}h${minutes}m${seconds}s`
|
||||
},
|
||||
toggleTwitchUrl() {
|
||||
if (this.usingTwitchUrl) {
|
||||
this.currentUrl = 'https://twitch.tv'
|
||||
} else {
|
||||
this.currentUrl = this.instanceUrl
|
||||
}
|
||||
},
|
||||
async copyUrl() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.currentUrl + this.path + this.query)
|
||||
console.log('Content copied to clipboard')
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err)
|
||||
}
|
||||
},
|
||||
toggleTime() {
|
||||
if (this.usingTime) {
|
||||
const timestamp = this.formatSecondsToQuery(this.$props.time)
|
||||
this.query = '?t=' + timestamp
|
||||
} else {
|
||||
this.query = ''
|
||||
}
|
||||
},
|
||||
gotoUrl() {
|
||||
location.href = this.currentUrl + this.path + this.query
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed top-0 bottom-0 left-0 right-0 flex w-full z-50 h-[100vh] bg-opacity-50 bg-black">
|
||||
<div class="bg-crust my-auto h-min mx-auto w-[35rem] max-w-[95vw] p-5 rounded-md relative z-50 text-contrast">
|
||||
<div class="flex justify-between">
|
||||
<h1 class="text-3xl font-bold">{{ $t('share.share') }}</h1>
|
||||
<button @click="$emit('close')">
|
||||
<v-icon name="io-close-sharp" scale="1.8"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
<hr class="my-2" />
|
||||
<div class="flex bg-surface0 p-3 rounded-md h-12 overflow-x-scroll whitespace-nowrap">
|
||||
<p class="" ref="urlPreview">
|
||||
{{ currentUrl + path + query }}
|
||||
</p>
|
||||
</div>
|
||||
<ul class="mt-1">
|
||||
<li>
|
||||
<label class="flex items-center">
|
||||
<input :disabled="!useTime" class="align-middle w-4 h-4 mr-1 disabled:opacity-50" type="checkbox" @change="toggleTime()" v-model="usingTime" /> {{ $t('share.addTimestamp') }}
|
||||
</label>
|
||||
<div
|
||||
class="fixed top-0 bottom-0 left-0 right-0 flex w-full z-50 h-[100vh] bg-opacity-50 bg-black"
|
||||
>
|
||||
<div
|
||||
class="bg-crust my-auto h-min mx-auto w-[35rem] max-w-[95vw] p-5 rounded-md relative z-50 text-contrast"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<h1 class="text-3xl font-bold">{{ $t('share.share') }}</h1>
|
||||
<button @click="$emit('close')">
|
||||
<v-icon name="io-close-sharp" scale="1.8"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
<hr class="my-2" />
|
||||
<div class="flex bg-surface0 p-3 rounded-md h-12 overflow-x-scroll whitespace-nowrap">
|
||||
<p class="" ref="urlPreview">
|
||||
{{ currentUrl + path + query }}
|
||||
</p>
|
||||
</div>
|
||||
<ul class="mt-1">
|
||||
<li>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
:disabled="!useTime"
|
||||
class="align-middle w-4 h-4 mr-1 disabled:opacity-50"
|
||||
type="checkbox"
|
||||
@change="toggleTime()"
|
||||
v-model="usingTime"
|
||||
/>
|
||||
{{ $t('share.addTimestamp') }}
|
||||
</label>
|
||||
|
||||
<label class="flex items-center">
|
||||
<input class="align-middle w-4 h-4 mr-1" @change="toggleTwitchUrl()" v-model="usingTwitchUrl" type="checkbox" /> {{ $t('share.twitchUrl') }}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
class="align-middle w-4 h-4 mr-1"
|
||||
@change="toggleTwitchUrl()"
|
||||
v-model="usingTwitchUrl"
|
||||
type="checkbox"
|
||||
/>
|
||||
{{ $t('share.twitchUrl') }}
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="space-x-2 mt-1">
|
||||
<button class="p-2 py-1.5 bg-surface0 rounded-md" @click="copyUrl()">{{ $t('share.copyLink') }}</button>
|
||||
<button class="p-2 py-1.5 bg-surface0 rounded-md" @click="gotoUrl()" >{{ $t('share.goto') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-x-2 mt-1">
|
||||
<button class="p-2 py-1.5 bg-surface0 rounded-md" @click="copyUrl()">
|
||||
{{ $t('share.copyLink') }}
|
||||
</button>
|
||||
<button class="p-2 py-1.5 bg-surface0 rounded-md" @click="gotoUrl()">
|
||||
{{ $t('share.goto') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,51 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { getSetting } from '@/settingsManager'
|
||||
import type { Social } from '@/types';
|
||||
import type { PropType } from 'vue';
|
||||
import type { Social } from '@/types'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
socials: {
|
||||
type: Object as PropType<Social[]>
|
||||
},
|
||||
about: {
|
||||
type: String
|
||||
}
|
||||
props: {
|
||||
socials: {
|
||||
type: Object as PropType<Social[]>
|
||||
},
|
||||
methods: {
|
||||
getSetting,
|
||||
getIconName(iconType: string) {
|
||||
console.log(iconType)
|
||||
const icons = ["Twitter", "instagram", "discord", "youtube", "tiktok", "reddit"]
|
||||
if (icons.includes(iconType)) {
|
||||
return "bi-" + iconType
|
||||
} else {
|
||||
return "bi-link-45deg"
|
||||
}
|
||||
}
|
||||
about: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getSetting,
|
||||
getIconName(iconType: string) {
|
||||
console.log(iconType)
|
||||
const icons = ['Twitter', 'instagram', 'discord', 'youtube', 'tiktok', 'reddit']
|
||||
if (icons.includes(iconType)) {
|
||||
return 'bi-' + iconType
|
||||
} else {
|
||||
return 'bi-link-45deg'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="getSetting('streamerAboutSectionVisible')" class="bg-primary 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>
|
||||
</div>
|
||||
|
||||
<p class="mb-5">{{ about || $t('streamer.noAbout') }}</p>
|
||||
|
||||
<hr class="my-auto w-full bg-gray-200 rounded-full opacity-40" />
|
||||
|
||||
<ul v-if="socials" class="flex font-semibold text-md justify-start flex-wrap flex-row">
|
||||
<li v-for="link in socials" :key="link.url">
|
||||
<a :href="link.url" class="text-contrast hover:text-neutral mr-4 flex">
|
||||
<v-icon :name="getIconName(link.type)" class="w-6 h-6 mr-1"></v-icon>
|
||||
<span>{{ link.name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p v-else> {{$t('streamer.noSocials')}} </p>
|
||||
<div
|
||||
v-if="getSetting('streamerAboutSectionVisible')"
|
||||
class="bg-primary 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p class="mb-5">{{ about || $t('streamer.noAbout') }}</p>
|
||||
|
||||
<hr class="my-auto w-full bg-gray-200 rounded-full opacity-40" />
|
||||
|
||||
<ul v-if="socials" class="flex font-semibold text-md justify-start flex-wrap flex-row">
|
||||
<li v-for="link in socials" :key="link.url">
|
||||
<a :href="link.url" class="text-contrast hover:text-neutral mr-4 flex">
|
||||
<v-icon :name="getIconName(link.type)" class="w-6 h-6 mr-1"></v-icon>
|
||||
<span>{{ link.name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p v-else>{{ $t('streamer.noSocials') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
Reference in New Issue
Block a user