Format
This commit is contained in:
parent
961d3bfa67
commit
4dd94d4af1
@ -15,9 +15,9 @@ const dev = import.meta.env.DEV
|
|||||||
<navbar-item></navbar-item>
|
<navbar-item></navbar-item>
|
||||||
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<RouterView :key="$route.fullPath"/>
|
<RouterView :key="$route.fullPath" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<footer-item></footer-item>
|
<footer-item></footer-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -72,7 +72,7 @@ export const createQualitySelector = (player: any) => {
|
|||||||
|
|
||||||
qualityLevels.on('addqualitylevel', () => {
|
qualityLevels.on('addqualitylevel', () => {
|
||||||
formatedQualities = qualityLevels.levels_.map((quality: QualityLevel) => {
|
formatedQualities = qualityLevels.levels_.map((quality: QualityLevel) => {
|
||||||
let qualityFramerate = ""
|
let qualityFramerate = ''
|
||||||
|
|
||||||
if (quality.frameRate > 30) {
|
if (quality.frameRate > 30) {
|
||||||
qualityFramerate = quality.frameRate
|
qualityFramerate = quality.frameRate
|
||||||
@ -86,9 +86,9 @@ export const createQualitySelector = (player: any) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// remove audio only
|
// remove audio only
|
||||||
const audioOnlyQuality = formatedQualities[formatedQualities.length-1]
|
const audioOnlyQuality = formatedQualities[formatedQualities.length - 1]
|
||||||
if (audioOnlyQuality.name === "undefinedp") {
|
if (audioOnlyQuality.name === 'undefinedp') {
|
||||||
formatedQualities.splice(formatedQualities.length-1, 1)
|
formatedQualities.splice(formatedQualities.length - 1, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedLevels.push(formatedQualities)
|
formattedLevels.push(formatedQualities)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<audio ref="videoPlayer" class="w-full" controls></audio>
|
<audio ref="videoPlayer" class="w-full" controls></audio>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// Importing video-js
|
// Importing video-js
|
||||||
@ -9,54 +9,53 @@ import Hls from 'hls.js'
|
|||||||
import { getSetting } from '@/settingsManager'
|
import { getSetting } from '@/settingsManager'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VideoJsPlayer',
|
name: 'VideoJsPlayer',
|
||||||
props: {
|
props: {
|
||||||
masterManifestUrl: {
|
masterManifestUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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>
|
</script>
|
||||||
|
|
@ -39,10 +39,9 @@ export default {
|
|||||||
|
|
||||||
<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
|
<span class="p-2.5 py-1.5 bg-surface0 rounded-md m-0.5 text-xs font-bold text-contrast">{{
|
||||||
class="p-2.5 py-1.5 bg-surface0 rounded-md m-0.5 text-xs font-bold text-contrast"
|
tag
|
||||||
>{{ tag }}</span
|
}}</span>
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
followStreamer() {
|
followStreamer() {
|
||||||
const username = this.$props.username
|
const username = this.$props.username
|
||||||
const follows = localStorage.getItem('following') || "[]"
|
const follows = localStorage.getItem('following') || '[]'
|
||||||
|
|
||||||
let parsedFollows: string[] = JSON.parse(follows)
|
let parsedFollows: string[] = JSON.parse(follows)
|
||||||
|
|
||||||
|
@ -4,12 +4,12 @@ export default {
|
|||||||
return {
|
return {
|
||||||
version: `${import.meta.env.SAFETWITCH_TAG}-${import.meta.env.SAFETWITCH_COMMIT_HASH}`
|
version: `${import.meta.env.SAFETWITCH_TAG}-${import.meta.env.SAFETWITCH_COMMIT_HASH}`
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
</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-contrast font-bold">SafeTwitch {{ version}}</p>
|
<p class="text-contrast font-bold">SafeTwitch {{ version }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -17,7 +17,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const savedLocale = localStorage.getItem('language')
|
const savedLocale = localStorage.getItem('language')
|
||||||
if(savedLocale) {
|
if (savedLocale) {
|
||||||
this.$i18n.locale = savedLocale
|
this.$i18n.locale = savedLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,16 +35,10 @@ export default {
|
|||||||
<search-bar class="mt-4 mr-4 hidden sm:inline-block sm:mt-0"></search-bar>
|
<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">
|
<div class="text-contrast hidden space-x-4 sm:block">
|
||||||
<a
|
<a href="https://codeberg.org/dragongoose/safetwitch" target="_blank">{{ $t('nav.code') }}</a>
|
||||||
href="https://codeberg.org/dragongoose/safetwitch"
|
|
||||||
target="_blank"
|
|
||||||
>{{ $t('nav.code') }}</a
|
|
||||||
>
|
|
||||||
<a :href="'https://twitch.tv' + $route.fullPath">Twitch</a>
|
<a :href="'https://twitch.tv' + $route.fullPath">Twitch</a>
|
||||||
<router-link to="/privacy">{{
|
<router-link to="/privacy">{{ $t('nav.privacy') }}</router-link>
|
||||||
$t('nav.privacy')
|
<router-link to="/settings">{{ $t('nav.settings') }}</router-link>
|
||||||
}}</router-link>
|
|
||||||
<router-link to="/settings">{{ $t("nav.settings") }}</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block sm:hidden">
|
<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://codeberg.org/dragongoose/safetwitch">{{ $t('nav.code') }}</a>
|
||||||
<a :href="'https://twitch.tv' + $route.fullPath">Twitch</a>
|
<a :href="'https://twitch.tv' + $route.fullPath">Twitch</a>
|
||||||
<router-link to="/privacy">{{ $t('nav.privacy') }}</router-link>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,9 +24,9 @@ export default {
|
|||||||
login: streamData.streamer.name,
|
login: streamData.streamer.name,
|
||||||
followers: 0,
|
followers: 0,
|
||||||
isLive: true,
|
isLive: true,
|
||||||
about: "",
|
about: '',
|
||||||
pfp: streamData.streamer.pfp,
|
pfp: streamData.streamer.pfp,
|
||||||
banner: "",
|
banner: '',
|
||||||
isPartner: false,
|
isPartner: false,
|
||||||
colorHex: streamData.streamer.colorHex,
|
colorHex: streamData.streamer.colorHex,
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -34,13 +34,13 @@ export default {
|
|||||||
title: streamData.title,
|
title: streamData.title,
|
||||||
tags: streamData.tags,
|
tags: streamData.tags,
|
||||||
startedAt: 0,
|
startedAt: 0,
|
||||||
topic: "",
|
topic: '',
|
||||||
viewers: streamData.viewers,
|
viewers: streamData.viewers,
|
||||||
preview: streamData.preview
|
preview: streamData.preview
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
const frontend_url = protocol + import.meta.env.SAFETWITCH_INSTANCE_DOMAIN
|
||||||
@ -68,7 +68,9 @@ export default {
|
|||||||
<div class="inline-flex">
|
<div class="inline-flex">
|
||||||
<img :src="data.pfp" class="rounded-full mr-2" />
|
<img :src="data.pfp" class="rounded-full mr-2" />
|
||||||
<div class="w-full">
|
<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">
|
<div class="inline-flex w-full justify-between">
|
||||||
<p class="text-neutral">{{ data.username }}</p>
|
<p class="text-neutral">{{ data.username }}</p>
|
||||||
<p class="self-end float-right">
|
<p class="self-end float-right">
|
||||||
|
@ -37,7 +37,7 @@ export default {
|
|||||||
createQualitySelector(this.player)
|
createQualitySelector(this.player)
|
||||||
|
|
||||||
if (this.$route.query['t']) {
|
if (this.$route.query['t']) {
|
||||||
const timeQuery = this.$route.query['t'].toString() || ""
|
const timeQuery = this.$route.query['t'].toString() || ''
|
||||||
const time = getTimeFromQuery(timeQuery)
|
const time = getTimeFromQuery(timeQuery)
|
||||||
this.player.currentTime(time)
|
this.player.currentTime(time)
|
||||||
}
|
}
|
||||||
|
@ -1,108 +1,129 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from 'vue-router'
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setup() {
|
setup() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const instanceUrl = location.protocol + '//' + location.host;
|
const instanceUrl = location.protocol + '//' + location.host
|
||||||
let currentUrl = ref(instanceUrl)
|
let currentUrl = ref(instanceUrl)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// remove any query from the path
|
// remove any query from the path
|
||||||
path: route.fullPath.split("?")[0],
|
path: route.fullPath.split('?')[0],
|
||||||
usingTwitchUrl: ref(false),
|
usingTwitchUrl: ref(false),
|
||||||
usingTime: ref(false),
|
usingTime: ref(false),
|
||||||
query: ref(""),
|
query: ref(''),
|
||||||
instanceUrl,
|
instanceUrl,
|
||||||
currentUrl
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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
|
||||||
<div class="bg-crust my-auto h-min mx-auto w-[35rem] max-w-[95vw] p-5 rounded-md relative z-50 text-contrast">
|
class="fixed top-0 bottom-0 left-0 right-0 flex w-full z-50 h-[100vh] bg-opacity-50 bg-black"
|
||||||
<div class="flex justify-between">
|
>
|
||||||
<h1 class="text-3xl font-bold">{{ $t('share.share') }}</h1>
|
<div
|
||||||
<button @click="$emit('close')">
|
class="bg-crust my-auto h-min mx-auto w-[35rem] max-w-[95vw] p-5 rounded-md relative z-50 text-contrast"
|
||||||
<v-icon name="io-close-sharp" scale="1.8"></v-icon>
|
>
|
||||||
</button>
|
<div class="flex justify-between">
|
||||||
</div>
|
<h1 class="text-3xl font-bold">{{ $t('share.share') }}</h1>
|
||||||
<hr class="my-2" />
|
<button @click="$emit('close')">
|
||||||
<div class="flex bg-surface0 p-3 rounded-md h-12 overflow-x-scroll whitespace-nowrap">
|
<v-icon name="io-close-sharp" scale="1.8"></v-icon>
|
||||||
<p class="" ref="urlPreview">
|
</button>
|
||||||
{{ currentUrl + path + query }}
|
</div>
|
||||||
</p>
|
<hr class="my-2" />
|
||||||
</div>
|
<div class="flex bg-surface0 p-3 rounded-md h-12 overflow-x-scroll whitespace-nowrap">
|
||||||
<ul class="mt-1">
|
<p class="" ref="urlPreview">
|
||||||
<li>
|
{{ currentUrl + path + query }}
|
||||||
<label class="flex items-center">
|
</p>
|
||||||
<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') }}
|
</div>
|
||||||
</label>
|
<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">
|
<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') }}
|
<input
|
||||||
</label>
|
class="align-middle w-4 h-4 mr-1"
|
||||||
</li>
|
@change="toggleTwitchUrl()"
|
||||||
</ul>
|
v-model="usingTwitchUrl"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
{{ $t('share.twitchUrl') }}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="space-x-2 mt-1">
|
<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="copyUrl()">
|
||||||
<button class="p-2 py-1.5 bg-surface0 rounded-md" @click="gotoUrl()" >{{ $t('share.goto') }}</button>
|
{{ $t('share.copyLink') }}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<button class="p-2 py-1.5 bg-surface0 rounded-md" @click="gotoUrl()">
|
||||||
|
{{ $t('share.goto') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
@ -1,51 +1,54 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getSetting } from '@/settingsManager'
|
import { getSetting } from '@/settingsManager'
|
||||||
import type { Social } from '@/types';
|
import type { Social } from '@/types'
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
socials: {
|
socials: {
|
||||||
type: Object as PropType<Social[]>
|
type: Object as PropType<Social[]>
|
||||||
},
|
|
||||||
about: {
|
|
||||||
type: String
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
about: {
|
||||||
getSetting,
|
type: String
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="getSetting('streamerAboutSectionVisible')" class="bg-primary mt-1 p-5 pt-3 rounded-lg w-full space-y-3">
|
<div
|
||||||
<div class="inline-flex w-full">
|
v-if="getSetting('streamerAboutSectionVisible')"
|
||||||
<span class="pr-3 font-bold text-3xl">{{ $t('streamer.about') }}</span>
|
class="bg-primary mt-1 p-5 pt-3 rounded-lg w-full space-y-3"
|
||||||
</div>
|
>
|
||||||
|
<div class="inline-flex w-full">
|
||||||
<p class="mb-5">{{ about || $t('streamer.noAbout') }}</p>
|
<span class="pr-3 font-bold text-3xl">{{ $t('streamer.about') }}</span>
|
||||||
|
|
||||||
<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>
|
</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>
|
||||||
</template>
|
</template>
|
@ -14,7 +14,6 @@ import fr from '@/locales/fr.json'
|
|||||||
import uk from '@/locales/uk.json'
|
import uk from '@/locales/uk.json'
|
||||||
import de from '@/locales/de.json'
|
import de from '@/locales/de.json'
|
||||||
|
|
||||||
|
|
||||||
export default createI18n({
|
export default createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: import.meta.env.VUE_APP_I18N_LOCALE || 'en-US',
|
locale: import.meta.env.VUE_APP_I18N_LOCALE || 'en-US',
|
||||||
@ -34,6 +33,6 @@ export default createI18n({
|
|||||||
'it-IT': it,
|
'it-IT': it,
|
||||||
'fr-FR': fr,
|
'fr-FR': fr,
|
||||||
'uk-UA': uk,
|
'uk-UA': uk,
|
||||||
'de-DE': de,
|
'de-DE': de
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,6 @@ 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 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) + '...'
|
||||||
@ -53,7 +52,7 @@ export async function getEndpoint(endpoint: string) {
|
|||||||
*/
|
*/
|
||||||
export function getTimeFromQuery(query: string) {
|
export function getTimeFromQuery(query: string) {
|
||||||
// H, M, S
|
// H, M, S
|
||||||
const x = query.split(/[^0-9.]/g);
|
const x = query.split(/[^0-9.]/g)
|
||||||
const times = x.map(Number)
|
const times = x.map(Number)
|
||||||
|
|
||||||
let time = 0
|
let time = 0
|
||||||
@ -62,4 +61,3 @@ export function getTimeFromQuery(query: string) {
|
|||||||
time += times[2]
|
time += times[2]
|
||||||
return time
|
return time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,60 @@
|
|||||||
const locales = ['en-US', 'es-ES', 'nl-NL', 'pt-PT', 'fa-IR', 'he-IL', 'ru-RU', 'ko-KR', 'cs-CZ', 'pl-PL', 'it-IT', 'fr-FR', 'uk-UA', 'de-DE']
|
const locales = [
|
||||||
const languages = ['English', 'Español', 'Nederlands', 'Português', 'فارسی', 'עִבְרִית', 'Русский', '한국어', 'Česky', 'Polski', 'Italia', 'Français', 'Українська', 'Deutsch']
|
'en-US',
|
||||||
|
'es-ES',
|
||||||
|
'nl-NL',
|
||||||
|
'pt-PT',
|
||||||
|
'fa-IR',
|
||||||
|
'he-IL',
|
||||||
|
'ru-RU',
|
||||||
|
'ko-KR',
|
||||||
|
'cs-CZ',
|
||||||
|
'pl-PL',
|
||||||
|
'it-IT',
|
||||||
|
'fr-FR',
|
||||||
|
'uk-UA',
|
||||||
|
'de-DE'
|
||||||
|
]
|
||||||
|
const languages = [
|
||||||
|
'English',
|
||||||
|
'Español',
|
||||||
|
'Nederlands',
|
||||||
|
'Português',
|
||||||
|
'فارسی',
|
||||||
|
'עִבְרִית',
|
||||||
|
'Русский',
|
||||||
|
'한국어',
|
||||||
|
'Česky',
|
||||||
|
'Polski',
|
||||||
|
'Italia',
|
||||||
|
'Français',
|
||||||
|
'Українська',
|
||||||
|
'Deutsch'
|
||||||
|
]
|
||||||
|
|
||||||
export interface SettingsCheckbox {
|
export interface SettingsCheckbox {
|
||||||
name: string
|
name: string
|
||||||
selected: boolean,
|
selected: boolean
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsOptions {
|
export interface SettingsOptions {
|
||||||
name: string
|
name: string
|
||||||
options: string[]
|
options: string[]
|
||||||
selected: string
|
selected: string
|
||||||
type: 'option'
|
type: 'option'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
version: string
|
version: string
|
||||||
settings: {
|
settings: {
|
||||||
audioOnly: SettingsCheckbox
|
audioOnly: SettingsCheckbox
|
||||||
defaultQuality: SettingsOptions
|
defaultQuality: SettingsOptions
|
||||||
language: SettingsOptions
|
language: SettingsOptions
|
||||||
chatVisible: SettingsCheckbox
|
chatVisible: SettingsCheckbox
|
||||||
streamTagsVisible: SettingsCheckbox
|
streamTagsVisible: SettingsCheckbox
|
||||||
streamerAboutSectionVisible: SettingsCheckbox
|
streamerAboutSectionVisible: SettingsCheckbox
|
||||||
autoplay: SettingsCheckbox
|
autoplay: SettingsCheckbox
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,10 +63,10 @@ export interface Settings {
|
|||||||
* @param i18n the i18n class in use
|
* @param i18n the i18n class in use
|
||||||
*/
|
*/
|
||||||
export const setLanguage = (selectedLanguage: string, i18n: any) => {
|
export const setLanguage = (selectedLanguage: string, i18n: any) => {
|
||||||
// Locales and languages in arrays to match them
|
// Locales and languages in arrays to match them
|
||||||
const locale = locales[languages.indexOf(selectedLanguage)]
|
const locale = locales[languages.indexOf(selectedLanguage)]
|
||||||
localStorage.setItem("language", locale)
|
localStorage.setItem('language', locale)
|
||||||
i18n.locale = locale
|
i18n.locale = locale
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,48 +74,48 @@ export const setLanguage = (selectedLanguage: string, i18n: any) => {
|
|||||||
* @returns Settings
|
* @returns Settings
|
||||||
*/
|
*/
|
||||||
export function getDefaultSettings(): Settings {
|
export function getDefaultSettings(): Settings {
|
||||||
return {
|
return {
|
||||||
version: import.meta.env.SAFETWITCH_TAG,
|
version: import.meta.env.SAFETWITCH_TAG,
|
||||||
settings: {
|
settings: {
|
||||||
audioOnly: {
|
audioOnly: {
|
||||||
name: 'audioOnly',
|
name: 'audioOnly',
|
||||||
selected: false,
|
selected: false,
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
},
|
},
|
||||||
defaultQuality: {
|
defaultQuality: {
|
||||||
name: 'defaultQuality',
|
name: 'defaultQuality',
|
||||||
options: ['160p', '360p', '480p', '720p', '1080p'],
|
options: ['160p', '360p', '480p', '720p', '1080p'],
|
||||||
selected: '480p',
|
selected: '480p',
|
||||||
type: 'option'
|
type: 'option'
|
||||||
},
|
},
|
||||||
language: {
|
language: {
|
||||||
name: 'language',
|
name: 'language',
|
||||||
options: languages,
|
options: languages,
|
||||||
selected: 'English',
|
selected: 'English',
|
||||||
type: 'option'
|
type: 'option'
|
||||||
},
|
},
|
||||||
chatVisible: {
|
chatVisible: {
|
||||||
name: 'chatVisible',
|
name: 'chatVisible',
|
||||||
selected: false,
|
selected: false,
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
},
|
},
|
||||||
streamTagsVisible: {
|
streamTagsVisible: {
|
||||||
name: 'streamTagsVisible',
|
name: 'streamTagsVisible',
|
||||||
selected: true,
|
selected: true,
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
},
|
},
|
||||||
streamerAboutSectionVisible: {
|
streamerAboutSectionVisible: {
|
||||||
name: 'streamerAboutSectionVisible',
|
name: 'streamerAboutSectionVisible',
|
||||||
selected: true,
|
selected: true,
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
},
|
},
|
||||||
autoplay: {
|
autoplay: {
|
||||||
name: 'autoplay',
|
name: 'autoplay',
|
||||||
selected: false,
|
selected: false,
|
||||||
type: 'checkbox'
|
type: 'checkbox'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,50 +123,51 @@ export function getDefaultSettings(): Settings {
|
|||||||
* @param settings Settings of the Settings type
|
* @param settings Settings of the Settings type
|
||||||
* @returns The synced settings and a boolean stating if the settings were modified
|
* @returns The synced settings and a boolean stating if the settings were modified
|
||||||
*/
|
*/
|
||||||
export function syncUserSettings(settings: Settings): {settings: Settings, changed: boolean}{
|
export function syncUserSettings(settings: Settings): { settings: Settings; changed: boolean } {
|
||||||
const defaultSettings = getDefaultSettings()
|
const defaultSettings = getDefaultSettings()
|
||||||
let userSettings = settings
|
let userSettings = settings
|
||||||
|
|
||||||
// converting settings storage from versions older
|
// converting settings storage from versions older
|
||||||
// than 2.4.1
|
// than 2.4.1
|
||||||
let oldMigration = false
|
let oldMigration = false
|
||||||
|
|
||||||
|
if (userSettings.version === import.meta.env.SAFETWITCH_TAG) {
|
||||||
|
console.log('Settings up to date!')
|
||||||
|
return { settings: userSettings, changed: false }
|
||||||
|
} else {
|
||||||
|
console.log('Settings outdated... Migrating')
|
||||||
|
// converts v2.4.1 to 241
|
||||||
|
const settingsVersion = Number(
|
||||||
|
userSettings.version.slice(1, defaultSettings.version.length).split('.').join('')
|
||||||
|
)
|
||||||
|
|
||||||
if (userSettings.version === import.meta.env.SAFETWITCH_TAG) {
|
if (settingsVersion < 241) {
|
||||||
console.log('Settings up to date!')
|
oldMigration = true
|
||||||
return { settings: userSettings, changed: false }
|
}
|
||||||
} else {
|
}
|
||||||
console.log('Settings outdated... Migrating')
|
|
||||||
// converts v2.4.1 to 241
|
|
||||||
const settingsVersion = Number(userSettings.version.slice(1, defaultSettings.version.length).split(".").join(""))
|
|
||||||
|
|
||||||
if (settingsVersion < 241) {
|
if (oldMigration) {
|
||||||
oldMigration = true
|
const oldSettings: any = userSettings
|
||||||
}
|
delete oldSettings.version
|
||||||
|
const migrated: Settings = {
|
||||||
|
version: defaultSettings.version,
|
||||||
|
settings: {
|
||||||
|
...oldSettings
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldMigration) {
|
userSettings = migrated
|
||||||
const oldSettings: any = userSettings
|
}
|
||||||
delete oldSettings.version
|
console.log(userSettings)
|
||||||
const migrated: Settings = {
|
|
||||||
version: defaultSettings.version,
|
|
||||||
settings: {
|
|
||||||
...oldSettings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
userSettings = migrated
|
const synced = { ...defaultSettings, ...userSettings }
|
||||||
}
|
|
||||||
console.log(userSettings)
|
|
||||||
|
|
||||||
const synced = { ...defaultSettings, ...userSettings }
|
// update avaliable languages
|
||||||
|
synced.settings.language.options = defaultSettings.settings.language.options
|
||||||
// update avaliable languages
|
synced.version = import.meta.env.SAFETWITCH_TAG
|
||||||
synced.settings.language.options = defaultSettings.settings.language.options
|
localStorage.setItem('settings', JSON.stringify(synced))
|
||||||
synced.version = import.meta.env.SAFETWITCH_TAG
|
console.log('Migrated!')
|
||||||
localStorage.setItem('settings', JSON.stringify(synced))
|
return { settings: synced, changed: true }
|
||||||
console.log('Migrated!')
|
|
||||||
return { settings: synced, changed: true }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,15 +178,15 @@ export function syncUserSettings(settings: Settings): {settings: Settings, chang
|
|||||||
* getSetting("audioOnly") // false
|
* getSetting("audioOnly") // false
|
||||||
*/
|
*/
|
||||||
export function getSetting(key: string): boolean | string {
|
export function getSetting(key: string): boolean | string {
|
||||||
const storage = localStorage.getItem('settings')
|
const storage = localStorage.getItem('settings')
|
||||||
let parsed
|
let parsed
|
||||||
if (!storage) {
|
if (!storage) {
|
||||||
parsed = getDefaultSettings()
|
parsed = getDefaultSettings()
|
||||||
} else {
|
} else {
|
||||||
parsed = JSON.parse(storage)
|
parsed = JSON.parse(storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed[key].selected
|
return parsed[key].selected
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,53 +195,53 @@ export function getSetting(key: string): boolean | string {
|
|||||||
* @default string light
|
* @default string light
|
||||||
*/
|
*/
|
||||||
export function getTheme() {
|
export function getTheme() {
|
||||||
return localStorage.getItem('theme') || "light"
|
return localStorage.getItem('theme') || 'light'
|
||||||
}
|
}
|
||||||
|
|
||||||
// every avaliable theme
|
// every avaliable theme
|
||||||
export const themeList = [
|
export const themeList = [
|
||||||
{
|
{
|
||||||
// name your theme anything that could be a valid css class name
|
// name your theme anything that could be a valid css class name
|
||||||
// remember what you named your theme because you will use it as a class to enable the theme
|
// remember what you named your theme because you will use it as a class to enable the theme
|
||||||
name: 'dark',
|
name: 'dark',
|
||||||
// put any overrides your theme has here
|
// put any overrides your theme has here
|
||||||
// just as if you were to extend tailwind's theme like normal https://tailwindcss.com/docs/theme#extending-the-default-theme
|
// just as if you were to extend tailwind's theme like normal https://tailwindcss.com/docs/theme#extending-the-default-theme
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
"primary": '#141515',
|
primary: '#141515',
|
||||||
"secondary": '#1e1f1f',
|
secondary: '#1e1f1f',
|
||||||
"overlay0": '#282a2a',
|
overlay0: '#282a2a',
|
||||||
"overlay1": '#323434',
|
overlay1: '#323434',
|
||||||
"surface0": '#393B3B',
|
surface0: '#393B3B',
|
||||||
"surface1": '#3F4242',
|
surface1: '#3F4242',
|
||||||
"crust": '#0C0C0C',
|
crust: '#0C0C0C',
|
||||||
"purple": '#D946EF',
|
purple: '#D946EF',
|
||||||
"red": "#980C0C",
|
red: '#980C0C',
|
||||||
"neutral": "#bdbdbd",
|
neutral: '#bdbdbd',
|
||||||
"contrast": "white",
|
contrast: 'white'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// name your theme anything that could be a valid css class name
|
|
||||||
// remember what you named your theme because you will use it as a class to enable the theme
|
|
||||||
name: 'light',
|
|
||||||
// put any overrides your theme has here
|
|
||||||
// just as if you were to extend tailwind's theme like normal https://tailwindcss.com/docs/theme#extending-the-default-theme
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
"primary": '#ebeaea',
|
|
||||||
"secondary": '#e1e0e0',
|
|
||||||
"overlay0": '#d7d5d5',
|
|
||||||
"overlay1": '#cdcbcb',
|
|
||||||
"surface0": '#c6c4c4',
|
|
||||||
"surface1": '#c0bdbd',
|
|
||||||
"crust": '#fafafa',
|
|
||||||
"purple": '#D946EF',
|
|
||||||
"red": "#e81304",
|
|
||||||
"neutral": "gray",
|
|
||||||
"contrast": "black",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// name your theme anything that could be a valid css class name
|
||||||
|
// remember what you named your theme because you will use it as a class to enable the theme
|
||||||
|
name: 'light',
|
||||||
|
// put any overrides your theme has here
|
||||||
|
// just as if you were to extend tailwind's theme like normal https://tailwindcss.com/docs/theme#extending-the-default-theme
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#ebeaea',
|
||||||
|
secondary: '#e1e0e0',
|
||||||
|
overlay0: '#d7d5d5',
|
||||||
|
overlay1: '#cdcbcb',
|
||||||
|
surface0: '#c6c4c4',
|
||||||
|
surface1: '#c0bdbd',
|
||||||
|
crust: '#fafafa',
|
||||||
|
purple: '#D946EF',
|
||||||
|
red: '#e81304',
|
||||||
|
neutral: 'gray',
|
||||||
|
contrast: 'black'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
@ -98,10 +98,9 @@ export default {
|
|||||||
|
|
||||||
<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
|
<span class="p-1 py-0.5 mr-1 text-sm font-bold bg-overlay1 rounded-sm">{{
|
||||||
class="p-1 py-0.5 mr-1 text-sm font-bold bg-overlay1 rounded-sm"
|
tag
|
||||||
>{{ tag }}</span
|
}}</span>
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -125,10 +124,9 @@ export default {
|
|||||||
|
|
||||||
<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
|
<span class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-overlay1 rounded-sm">{{
|
||||||
class="text-white p-1 py-0.5 mr-1 text-sm font-bold bg-overlay1 rounded-sm"
|
tag
|
||||||
>{{ tag }}</span
|
}}</span>
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,7 +30,7 @@ export default {
|
|||||||
status.value = 'error'
|
status.value = 'error'
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(srcUrl)
|
console.log(srcUrl)
|
||||||
|
|
||||||
const videoOptions = {
|
const videoOptions = {
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
@ -96,8 +96,7 @@ export default {
|
|||||||
class="flex bg-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-contrast"
|
class="flex bg-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-contrast"
|
||||||
>
|
>
|
||||||
<div class="w-full mx-auto rounded-lg mb-5">
|
<div class="w-full mx-auto rounded-lg mb-5">
|
||||||
<video-player :options="videoOptions">
|
<video-player :options="videoOptions"> </video-player>
|
||||||
</video-player>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex-wrap md:p-3">
|
<div class="w-full flex-wrap md:p-3">
|
||||||
@ -122,23 +121,23 @@ export default {
|
|||||||
|
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<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">
|
<p class="align-baseline font-bold ml-3">
|
||||||
{{ abbreviate(data.streamer.followers) }} {{ $t('main.followers') }}
|
{{ abbreviate(data.streamer.followers) }} {{ $t('main.followers') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-x-1">
|
<div class="space-x-1">
|
||||||
<a :href="srcUrl" download>
|
<a :href="srcUrl" download>
|
||||||
<button class="px-2 py-1.5 rounded-lg bg-purple">
|
<button class="px-2 py-1.5 rounded-lg bg-purple">
|
||||||
<v-icon name="md-download-round"></v-icon>
|
<v-icon name="md-download-round"></v-icon>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple">
|
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple">
|
||||||
<v-icon name="fa-share-alt"></v-icon>
|
<v-icon name="fa-share-alt"></v-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -102,11 +102,7 @@ export default {
|
|||||||
<h1 class="font-bold text-5xl">Following</h1>
|
<h1 class="font-bold text-5xl">Following</h1>
|
||||||
<p class="text-xl">Streamers you follow</p>
|
<p class="text-xl">Streamers you follow</p>
|
||||||
<ul class="flex overflow-x-scroll space-x-2 flex-nowrap h-[22rem] items-center">
|
<ul class="flex overflow-x-scroll space-x-2 flex-nowrap h-[22rem] items-center">
|
||||||
<li
|
<li v-for="streamer in following" :key="streamer" class="inline-block">
|
||||||
v-for="streamer in following"
|
|
||||||
:key="streamer"
|
|
||||||
class="inline-block"
|
|
||||||
>
|
|
||||||
<stream-preview-vue :name="streamer"></stream-preview-vue>
|
<stream-preview-vue :name="streamer"></stream-preview-vue>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -18,7 +18,7 @@ export default {
|
|||||||
settings = syncResp.settings
|
settings = syncResp.settings
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedTheme = localStorage.getItem('theme') || "light"
|
let selectedTheme = localStorage.getItem('theme') || 'light'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
@ -35,7 +35,7 @@ export default {
|
|||||||
|
|
||||||
this.setTheme()
|
this.setTheme()
|
||||||
// Reload needed
|
// Reload needed
|
||||||
location.href = "/"
|
location.href = '/'
|
||||||
},
|
},
|
||||||
setTheme() {
|
setTheme() {
|
||||||
localStorage.setItem('theme', this.selectedTheme)
|
localStorage.setItem('theme', this.selectedTheme)
|
||||||
@ -44,15 +44,15 @@ export default {
|
|||||||
if (this.selectedTheme == theme) {
|
if (this.selectedTheme == theme) {
|
||||||
return 'border-purple'
|
return 'border-purple'
|
||||||
}
|
}
|
||||||
return "border-none"
|
return 'border-none'
|
||||||
},
|
},
|
||||||
download() {
|
download() {
|
||||||
var hiddenElement = document.createElement('a');
|
var hiddenElement = document.createElement('a')
|
||||||
|
|
||||||
hiddenElement.href = 'data:attachment/text,' + encodeURI(JSON.stringify(this.settings));
|
hiddenElement.href = 'data:attachment/text,' + encodeURI(JSON.stringify(this.settings))
|
||||||
hiddenElement.target = '_blank';
|
hiddenElement.target = '_blank'
|
||||||
hiddenElement.download = 'safetwitch_prefs.json';
|
hiddenElement.download = 'safetwitch_prefs.json'
|
||||||
hiddenElement.click();
|
hiddenElement.click()
|
||||||
},
|
},
|
||||||
async handleImport(event: any) {
|
async handleImport(event: any) {
|
||||||
const file = await event.target.files[0].text()
|
const file = await event.target.files[0].text()
|
||||||
@ -68,14 +68,14 @@ export default {
|
|||||||
|
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.save()
|
this.save()
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mx-auto w-[35rem] max-w-[95vw] p-5 py-3 bg-secondary rounded-md text-contrast">
|
<div class="mx-auto w-[35rem] max-w-[95vw] p-5 py-3 bg-secondary rounded-md text-contrast">
|
||||||
<h1 class="font-bold text-3xl">{{ $t("nav.settings") }}</h1>
|
<h1 class="font-bold text-3xl">{{ $t('nav.settings') }}</h1>
|
||||||
<hr class="my-2" />
|
<hr class="my-2" />
|
||||||
<ul class="w-full space-y-1">
|
<ul class="w-full space-y-1">
|
||||||
<li v-for="setting in settings.settings" :key="setting.type">
|
<li v-for="setting in settings.settings" :key="setting.type">
|
||||||
@ -86,8 +86,12 @@ export default {
|
|||||||
|
|
||||||
<div v-else-if="setting.type == 'option'" class="justify-between items-center flex">
|
<div v-else-if="setting.type == 'option'" class="justify-between items-center flex">
|
||||||
<label :for="setting.name">{{ $t(`settings.${setting.name}`) }}</label>
|
<label :for="setting.name">{{ $t(`settings.${setting.name}`) }}</label>
|
||||||
<select :name="setting.name" type="checkbox" v-model="setting.selected"
|
<select
|
||||||
class="text-black rounded-md h-8 p-0 pr-8 pl-2">
|
:name="setting.name"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="setting.selected"
|
||||||
|
class="text-black rounded-md h-8 p-0 pr-8 pl-2"
|
||||||
|
>
|
||||||
<option v-for="option of setting.options" :key="option" :value="option">
|
<option v-for="option of setting.options" :key="option" :value="option">
|
||||||
{{ option }}
|
{{ option }}
|
||||||
</option>
|
</option>
|
||||||
@ -96,25 +100,40 @@ export default {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h1 class="font-bold text-3xl mt-2">{{ $t("main.themes") }}</h1>
|
<h1 class="font-bold text-3xl mt-2">{{ $t('main.themes') }}</h1>
|
||||||
<hr class="my-2" />
|
<hr class="my-2" />
|
||||||
<ul class="flex space-x-2 ">
|
<ul class="flex space-x-2">
|
||||||
<!--
|
<!--
|
||||||
Use theme colors for preview
|
Use theme colors for preview
|
||||||
-->
|
-->
|
||||||
<li v-for="theme in themeList" :key="theme.name" class="hover:scale-110 border-2 rounded-md transition-transform"
|
<li
|
||||||
:class="highlight(theme.name)">
|
v-for="theme in themeList"
|
||||||
<button @click="selectedTheme = theme.name" class="p-5 py-1.5 border-4 rounded-md"
|
:key="theme.name"
|
||||||
:style="`border-color: ${theme.extend.colors.primary}; background:${theme.extend.colors.crust}; color:${theme.extend.colors.contrast};`">
|
class="hover:scale-110 border-2 rounded-md transition-transform"
|
||||||
|
:class="highlight(theme.name)"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
@click="selectedTheme = theme.name"
|
||||||
|
class="p-5 py-1.5 border-4 rounded-md"
|
||||||
|
:style="`border-color: ${theme.extend.colors.primary}; background:${theme.extend.colors.crust}; color:${theme.extend.colors.contrast};`"
|
||||||
|
>
|
||||||
<p>{{ theme.name }}</p>
|
<p>{{ theme.name }}</p>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="space-x-2 mt-3">
|
<div class="space-x-2 mt-3">
|
||||||
<button @click="save" class="bg-surface0 p-4 py-2 rounded-md">{{ $t('settings.saveButton') }}</button>
|
<button @click="save" class="bg-surface0 p-4 py-2 rounded-md">
|
||||||
|
{{ $t('settings.saveButton') }}
|
||||||
|
</button>
|
||||||
<button @click="download" class="bg-surface0 p-4 py-2 rounded-md">Export</button>
|
<button @click="download" class="bg-surface0 p-4 py-2 rounded-md">Export</button>
|
||||||
<input type="file" @change="handleImport" name="fileinput" ref="fileinput" class="bg-surface0 p-4 py-2 rounded-md">
|
<input
|
||||||
|
type="file"
|
||||||
|
@change="handleImport"
|
||||||
|
name="fileinput"
|
||||||
|
ref="fileinput"
|
||||||
|
class="bg-surface0 p-4 py-2 rounded-md"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -12,7 +12,6 @@ import AudioPlayer from '@/components/AudioPlayer.vue'
|
|||||||
import AboutTab from '@/components/user/AboutTab.vue'
|
import AboutTab from '@/components/user/AboutTab.vue'
|
||||||
import ShareModal from '@/components/popups/ShareButtonModal.vue'
|
import ShareModal from '@/components/popups/ShareButtonModal.vue'
|
||||||
|
|
||||||
|
|
||||||
import type { StreamerData } from '@/types'
|
import type { StreamerData } from '@/types'
|
||||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||||
import { chatVisible, getSetting } from '@/settingsManager'
|
import { chatVisible, getSetting } from '@/settingsManager'
|
||||||
@ -26,7 +25,7 @@ export default {
|
|||||||
const status = ref<'ok' | 'error'>()
|
const status = ref<'ok' | 'error'>()
|
||||||
const rootBackendUrl = inject('rootBackendUrl')
|
const rootBackendUrl = inject('rootBackendUrl')
|
||||||
const videoOptions = {
|
const videoOptions = {
|
||||||
autoplay: getSetting("autoplay"),
|
autoplay: getSetting('autoplay'),
|
||||||
controls: true,
|
controls: true,
|
||||||
sources: [
|
sources: [
|
||||||
{
|
{
|
||||||
@ -48,9 +47,9 @@ export default {
|
|||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
// check if audio only
|
// check if audio only
|
||||||
const audioOnly = getSetting("audioOnly")
|
const audioOnly = getSetting('audioOnly')
|
||||||
if (audioOnly) {
|
if (audioOnly) {
|
||||||
this.$router.push({ query: { "audio-only": "true" } })
|
this.$router.push({ query: { 'audio-only': 'true' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = this.$route.params.username
|
const username = this.$route.params.username
|
||||||
@ -87,7 +86,12 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<share-modal v-if="shareModalVisible" :time="0" :useTime="false" @close="toggleShareModal"></share-modal>
|
<share-modal
|
||||||
|
v-if="shareModalVisible"
|
||||||
|
:time="0"
|
||||||
|
:useTime="false"
|
||||||
|
@close="toggleShareModal"
|
||||||
|
></share-modal>
|
||||||
<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>
|
||||||
|
|
||||||
@ -99,7 +103,8 @@ export default {
|
|||||||
class="flex bg-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-contrast"
|
class="flex bg-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-contrast"
|
||||||
>
|
>
|
||||||
<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 v-if="Boolean($route.query['audio-only']) === false" :options="videoOptions"> </video-player>
|
<video-player v-if="Boolean($route.query['audio-only']) === false" :options="videoOptions">
|
||||||
|
</video-player>
|
||||||
<audio-player v-else :masterManifestUrl="audioOptions"></audio-player>
|
<audio-player v-else :masterManifestUrl="audioOptions"></audio-player>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -176,7 +181,7 @@ export default {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple">
|
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple">
|
||||||
<v-icon name="fa-share-alt"></v-icon>
|
<v-icon name="fa-share-alt"></v-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,6 @@ import LoadingScreen from '@/components/LoadingScreen.vue'
|
|||||||
import AboutTab from '@/components/user/AboutTab.vue'
|
import AboutTab from '@/components/user/AboutTab.vue'
|
||||||
import ShareModal from '@/components/popups/ShareButtonModal.vue'
|
import ShareModal from '@/components/popups/ShareButtonModal.vue'
|
||||||
|
|
||||||
|
|
||||||
import type { Video } from '@/types'
|
import type { Video } from '@/types'
|
||||||
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
import { truncate, abbreviate, getEndpoint } from '@/mixins'
|
||||||
import { chatVisible, getSetting } from '@/settingsManager'
|
import { chatVisible, getSetting } from '@/settingsManager'
|
||||||
@ -83,7 +82,12 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<share-modal v-if="shareModalVisible" :time="time" :useTime="true" @close="toggleShareModal"></share-modal>
|
<share-modal
|
||||||
|
v-if="shareModalVisible"
|
||||||
|
:time="time"
|
||||||
|
:useTime="true"
|
||||||
|
@close="toggleShareModal"
|
||||||
|
></share-modal>
|
||||||
<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>
|
||||||
|
|
||||||
@ -127,10 +131,9 @@ export default {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple">
|
||||||
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple">
|
<v-icon name="fa-share-alt"></v-icon>
|
||||||
<v-icon name="fa-share-alt"></v-icon>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user