0
Fork 0
mirror of https://codeberg.org/SafeTwitch/safetwitch.git synced 2024-12-22 13:22:58 -05:00

Add share feature

This commit is contained in:
dragongoose 2023-09-16 18:13:28 -04:00
parent 62f3d8b195
commit ee3d2ba346
No known key found for this signature in database
GPG key ID: 01397EEC371CDAA5
7 changed files with 194 additions and 46 deletions

View file

@ -8,12 +8,12 @@
import videojs from 'video.js' import videojs from 'video.js'
import qualityLevels from 'videojs-contrib-quality-levels' import qualityLevels from 'videojs-contrib-quality-levels'
import { createQualitySelector } from '@/assets/qualitySelector' import { createQualitySelector } from '@/assets/qualitySelector'
import { getTimeFromQuery } from '@/mixins'
import 'video.js/dist/video-js.css' import 'video.js/dist/video-js.css'
videojs.registerPlugin('qualityLevels', qualityLevels) videojs.registerPlugin('qualityLevels', qualityLevels)
export default { export default {
name: 'VideoJsPlayer',
props: { props: {
options: { options: {
type: Object, type: Object,
@ -23,7 +23,7 @@ export default {
} }
}, },
emits: ['PlayerTimeUpdate'], emits: ['PlayerTimeUpdate'],
data() { setup() {
let player: any let player: any
return { return {
player player
@ -36,6 +36,12 @@ export default {
this.player = videojs('video-player', this.options, () => { this.player = videojs('video-player', this.options, () => {
createQualitySelector(this.player) createQualitySelector(this.player)
if (this.$route.query['t']) {
const timeQuery = this.$route.query['t'].toString() || ""
const time = getTimeFromQuery(timeQuery)
this.player.currentTime(time)
}
this.player.on('timeupdate', () => { this.player.on('timeupdate', () => {
emit('PlayerTimeUpdate', this.player.currentTime()) emit('PlayerTimeUpdate', this.player.currentTime())
}) })

View file

@ -0,0 +1,107 @@
<script lang="ts">
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)
return {
path: route.fullPath,
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-ctp-crust my-auto h-min mx-auto w-[35rem] max-w-[95vw] p-5 rounded-md relative z-50 text-white">
<div class="flex justify-between">
<h1 class="text-3xl font-bold">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-ctp-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" /> Add Timestamp
</label>
<label class="flex items-center">
<input class="align-middle w-4 h-4 mr-1" @change="toggleTwitchUrl()" v-model="usingTwitchUrl" type="checkbox" /> Twitch URL
</label>
</li>
</ul>
<div class="space-x-2 mt-1">
<button class="p-2 py-1.5 bg-ctp-surface0 rounded-md" @click="copyUrl()">Copy Link</button>
<button class="p-2 py-1.5 bg-ctp-surface0 rounded-md" @click="gotoUrl()" >Go to</button>
</div>
</div>
</div>
</template>

View file

@ -40,3 +40,15 @@ export async function getEndpoint(endpoint: string) {
return data return data
} }
export function getTimeFromQuery(query: string) {
// H, M, S
const x = query.split(/[^0-9.]/g);
const times = x.map(Number)
let time = 0
time += times[0] * 3600
time += times[1] * 60
time += times[2]
return time
}

View file

@ -53,7 +53,7 @@ export function getDefaultSettings() {
export function syncUserSettings() { export function syncUserSettings() {
const defaultSettings = getDefaultSettings() const defaultSettings = getDefaultSettings()
let userSettings = localStorage.getItem('settings') const userSettings = localStorage.getItem('settings')
if (!userSettings) return if (!userSettings) return
const parsedUserSettings = JSON.parse(userSettings) const parsedUserSettings = JSON.parse(userSettings)

View file

@ -7,15 +7,12 @@ import ErrorMessage from '@/components/ErrorMessage.vue'
import FollowButton from '@/components/FollowButton.vue' import FollowButton from '@/components/FollowButton.vue'
import LoadingScreen from '@/components/LoadingScreen.vue' import LoadingScreen from '@/components/LoadingScreen.vue'
import AboutTab from '@/components/user/AboutTab.vue' import AboutTab from '@/components/user/AboutTab.vue'
import ShareModal from '@/components/popups/ShareButtonPopup.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 } from '@/settingsManager' import { chatVisible } from '@/settingsManager'
interface ChatComponent {
updateVodComments: (time: number) => void
}
export default { export default {
inject: ['rootBackendUrl'], inject: ['rootBackendUrl'],
async setup() { async setup() {
@ -50,7 +47,9 @@ export default {
return { return {
data, data,
status, status,
videoOptions videoOptions,
time: ref(0),
shareModalVisible: ref(false)
} }
}, },
async mounted() { async mounted() {
@ -70,22 +69,22 @@ export default {
ErrorMessage, ErrorMessage,
FollowButton, FollowButton,
LoadingScreen, LoadingScreen,
AboutTab AboutTab,
ShareModal
}, },
methods: { methods: {
truncate, truncate,
abbreviate, abbreviate,
handlePlayerTimeUpdate(time: number) { chatVisible,
if (!chatVisible()) return toggleShareModal() {
const chat = this.$refs.chat as ChatComponent this.shareModalVisible = !this.shareModalVisible
chat.updateVodComments(time) }
},
chatVisible
} }
} }
</script> </script>
<template> <template>
<share-modal v-if="shareModalVisible" :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>
@ -97,7 +96,7 @@ export default {
class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white" class="flex bg-ctp-crust flex-col p-6 rounded-lg w-[99vw] md:max-w-prose md:min-w-[65ch] lg:max-w-[70rem] text-white"
> >
<div class="w-full mx-auto rounded-lg mb-5"> <div class="w-full mx-auto rounded-lg mb-5">
<video-player :options="videoOptions" @PlayerTimeUpdate="handlePlayerTimeUpdate"> <video-player :options="videoOptions">
</video-player> </video-player>
</div> </div>
@ -121,25 +120,22 @@ export default {
</div> </div>
</div> </div>
<div class="pt-2 inline-flex"> <div class="flex justify-between items-center">
<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>
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple-600">
<v-icon name="fa-share-alt"></v-icon>
</button>
</div>
</div> </div>
<!-- ABOUT TAB --> <!-- ABOUT TAB -->
<about-tab :socials="data.streamer.socials" :about="data.streamer.about"></about-tab> <about-tab :socials="data.streamer.socials" :about="data.streamer.about"></about-tab>
</div> </div>
<!--
<twitch-chat
v-if="chatVisible()"
:isVod="true"
:channelName="data.streamer.login"
ref="chat"
></twitch-chat>
-->
</div> </div>
</template> </template>

View file

@ -10,6 +10,8 @@ import LoadingScreen from '@/components/LoadingScreen.vue'
import VideoTab from '@/components/user/VideoTab.vue' import VideoTab from '@/components/user/VideoTab.vue'
import AudioPlayer from '@/components/AudioPlayer.vue' 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/ShareButtonPopup.vue'
import type { StreamerData } from '@/types' import type { StreamerData } from '@/types'
import { truncate, abbreviate, getEndpoint } from '@/mixins' import { truncate, abbreviate, getEndpoint } from '@/mixins'
@ -40,7 +42,8 @@ export default {
data, data,
status, status,
videoOptions, videoOptions,
audioOptions audioOptions,
shareModalVisible: ref(false)
} }
}, },
async mounted() { async mounted() {
@ -68,18 +71,23 @@ export default {
LoadingScreen, LoadingScreen,
VideoTab, VideoTab,
AudioPlayer, AudioPlayer,
AboutTab AboutTab,
ShareModal
}, },
methods: { methods: {
truncate, truncate,
abbreviate, abbreviate,
chatVisible, chatVisible,
getSetting getSetting,
toggleShareModal() {
this.shareModalVisible = !this.shareModalVisible
}
} }
} }
</script> </script>
<template> <template>
<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>
@ -160,11 +168,16 @@ export default {
</div> </div>
</div> </div>
<div class="pt-2 inline-flex"> <div class="pt-2 space-x-2 items-center inline-flex">
<follow-button :username="data.login"></follow-button> <follow-button :username="data.login"></follow-button>
<p class="align-baseline font-bold ml-3"> <p class="align-baseline font-bold ml-3">
{{ abbreviate(data.followers) }} {{ $t('main.followers') }} {{ abbreviate(data.followers) }} {{ $t('main.followers') }}
</p> </p>
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple-600">
<v-icon name="fa-share-alt"></v-icon>
</button>
</div> </div>
</div> </div>

View file

@ -8,6 +8,8 @@ import ErrorMessage from '@/components/ErrorMessage.vue'
import FollowButton from '@/components/FollowButton.vue' import FollowButton from '@/components/FollowButton.vue'
import LoadingScreen from '@/components/LoadingScreen.vue' import LoadingScreen from '@/components/LoadingScreen.vue'
import AboutTab from '@/components/user/AboutTab.vue' import AboutTab from '@/components/user/AboutTab.vue'
import ShareModal from '@/components/popups/ShareButtonPopup.vue'
import type { Video } from '@/types' import type { Video } from '@/types'
import { truncate, abbreviate, getEndpoint } from '@/mixins' import { truncate, abbreviate, getEndpoint } from '@/mixins'
@ -22,11 +24,9 @@ export default {
async setup() { async setup() {
const route = useRoute() const route = useRoute()
const vodID = route.params.vodID const vodID = route.params.vodID
const data = ref<Video>()
const status = ref<'ok' | 'error'>()
const rootBackendUrl = inject('rootBackendUrl') const rootBackendUrl = inject('rootBackendUrl')
const videoOptions = { const videoOptions = {
autoplay: true, autoplay: getSetting('autoplay'),
controls: true, controls: true,
sources: [ sources: [
{ {
@ -36,11 +36,12 @@ export default {
], ],
fluid: true fluid: true
} }
return { return {
data, data: ref<Video>(),
status, status: ref<'ok' | 'error'>(),
videoOptions videoOptions,
time: ref(0),
shareModalVisible: ref(false)
} }
}, },
async mounted() { async mounted() {
@ -60,7 +61,8 @@ export default {
ErrorMessage, ErrorMessage,
FollowButton, FollowButton,
LoadingScreen, LoadingScreen,
AboutTab AboutTab,
ShareModal
}, },
methods: { methods: {
truncate, truncate,
@ -68,15 +70,20 @@ export default {
handlePlayerTimeUpdate(time: number) { handlePlayerTimeUpdate(time: number) {
if (!chatVisible()) return if (!chatVisible()) return
const chat = this.$refs.chat as ChatComponent const chat = this.$refs.chat as ChatComponent
this.time = time
chat.updateVodComments(time) chat.updateVodComments(time)
}, },
chatVisible, chatVisible,
getSetting getSetting,
toggleShareModal() {
this.shareModalVisible = !this.shareModalVisible
}
} }
} }
</script> </script>
<template> <template>
<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>
@ -102,7 +109,7 @@ export default {
/> />
</router-link> </router-link>
<div class="ml-3 content-between"> <div class="ml-3 content-between w-5/6">
<router-link :to="'/' + data.streamer.login"> <router-link :to="'/' + data.streamer.login">
<h1 class="text-2xl md:text-4xl font-bold">{{ data.streamer.username }}</h1> <h1 class="text-2xl md:text-4xl font-bold">{{ data.streamer.username }}</h1>
</router-link> </router-link>
@ -112,11 +119,18 @@ export default {
</div> </div>
</div> </div>
<div class="pt-2 inline-flex"> <div class="flex justify-between items-center">
<follow-button :username="data.streamer.login"></follow-button> <div class="pt-2 inline-flex">
<p class="align-baseline font-bold ml-3"> <follow-button :username="data.streamer.login"></follow-button>
{{ abbreviate(data.streamer.followers) }} {{ $t('main.followers') }} <p class="align-baseline font-bold ml-3">
</p> {{ abbreviate(data.streamer.followers) }} {{ $t('main.followers') }}
</p>
</div>
<button @click="toggleShareModal" class="px-2 py-1.5 rounded-lg bg-purple-600">
<v-icon name="fa-share-alt"></v-icon>
</button>
</div> </div>
</div> </div>
@ -125,7 +139,7 @@ export default {
</div> </div>
<twitch-chat <twitch-chat
v-if="getSetting('chatVisible')" v-if="!getSetting('chatVisible')"
:isVod="true" :isVod="true"
:channelName="data.streamer.login" :channelName="data.streamer.login"
ref="chat" ref="chat"