mirror of https://github.com/FreeTubeApp/FreeTube
Add Live tab to channel pages (#3273)
* Add Live tab to channel pages * Handle invidious streams tab URL * Clear live videos when navigating between channels * Reset sort by when changing channels
This commit is contained in:
parent
a878ff395a
commit
0212467419
|
@ -315,7 +315,7 @@ const actions = {
|
|||
let urlType = 'unknown'
|
||||
|
||||
const channelPattern =
|
||||
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|playlists|about|community|channels))?\/?$/
|
||||
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|live|streams|playlists|about|community|channels))?\/?$/
|
||||
|
||||
const typePatterns = new Map([
|
||||
['playlist', /^(\/playlist\/?|\/embed(\/?videoseries)?)$/],
|
||||
|
@ -421,6 +421,10 @@ const actions = {
|
|||
|
||||
let subPath = null
|
||||
switch (match.groups.tab) {
|
||||
case 'live':
|
||||
case 'streams':
|
||||
subPath = 'live'
|
||||
break
|
||||
case 'playlists':
|
||||
subPath = 'playlists'
|
||||
break
|
||||
|
|
|
@ -59,6 +59,7 @@ export default defineComponent({
|
|||
subCount: 0,
|
||||
searchPage: 2,
|
||||
videoContinuationData: null,
|
||||
liveContinuationData: null,
|
||||
playlistContinuationData: null,
|
||||
searchContinuationData: null,
|
||||
communityContinuationData: null,
|
||||
|
@ -68,10 +69,12 @@ export default defineComponent({
|
|||
joined: 0,
|
||||
location: null,
|
||||
videoSortBy: 'newest',
|
||||
liveSortBy: 'newest',
|
||||
playlistSortBy: 'newest',
|
||||
lastSearchQuery: '',
|
||||
relatedChannels: [],
|
||||
latestVideos: [],
|
||||
latestLive: [],
|
||||
latestPlaylists: [],
|
||||
latestCommunityPosts: [],
|
||||
searchResults: [],
|
||||
|
@ -81,19 +84,13 @@ export default defineComponent({
|
|||
errorMessage: '',
|
||||
showSearchBar: true,
|
||||
showShareMenu: true,
|
||||
videoSelectValues: [
|
||||
videoShortLiveSelectValues: [
|
||||
'newest',
|
||||
'popular'
|
||||
],
|
||||
playlistSelectValues: [
|
||||
'newest',
|
||||
'last'
|
||||
],
|
||||
tabInfoValues: [
|
||||
'videos',
|
||||
'playlists',
|
||||
'community',
|
||||
'about'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -152,7 +149,7 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
videoSelectNames: function () {
|
||||
videoShortLiveSelectNames: function () {
|
||||
return [
|
||||
this.$t('Channel.Videos.Sort Types.Newest'),
|
||||
this.$t('Channel.Videos.Sort Types.Most Popular')
|
||||
|
@ -185,6 +182,8 @@ export default defineComponent({
|
|||
switch (this.currentTab) {
|
||||
case 'videos':
|
||||
return !isNullOrEmpty(this.videoContinuationData)
|
||||
case 'live':
|
||||
return !isNullOrEmpty(this.liveContinuationData)
|
||||
case 'playlists':
|
||||
return !isNullOrEmpty(this.playlistContinuationData)
|
||||
case 'community':
|
||||
|
@ -209,6 +208,28 @@ export default defineComponent({
|
|||
|
||||
hideSharingActions: function () {
|
||||
return this.$store.getters.getHideSharingActions
|
||||
},
|
||||
|
||||
hideLiveStreams: function () {
|
||||
return this.$store.getters.getHideLiveStreams
|
||||
},
|
||||
|
||||
tabInfoValues: function () {
|
||||
const values = [
|
||||
'videos',
|
||||
'live',
|
||||
'playlists',
|
||||
'community',
|
||||
'about'
|
||||
]
|
||||
|
||||
// remove tabs from the array based on user settings
|
||||
if (this.hideLiveStreams) {
|
||||
const index = values.indexOf('live')
|
||||
values.splice(index, 1)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -222,21 +243,30 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
this.id = this.$route.params.id
|
||||
this.currentTab = this.$route.params.currentTab ?? 'videos'
|
||||
let currentTab = this.$route.params.currentTab ?? 'videos'
|
||||
this.searchPage = 2
|
||||
this.relatedChannels = []
|
||||
this.latestVideos = []
|
||||
this.latestLive = []
|
||||
this.liveSortBy = 'newest'
|
||||
this.latestPlaylists = []
|
||||
this.searchResults = []
|
||||
this.shownElementList = []
|
||||
this.apiUsed = ''
|
||||
this.channelInstance = ''
|
||||
this.videoContinuationData = null
|
||||
this.liveContinuationData = null
|
||||
this.playlistContinuationData = null
|
||||
this.searchContinuationData = null
|
||||
this.communityContinuationData = null
|
||||
this.showSearchBar = true
|
||||
|
||||
if (this.hideLiveStreams && currentTab === 'live') {
|
||||
currentTab = 'videos'
|
||||
}
|
||||
|
||||
this.currentTab = currentTab
|
||||
|
||||
if (this.id === '@@@') {
|
||||
this.showShareMenu = false
|
||||
this.setErrorMessage(this.$i18n.t('Channel.This channel does not exist'))
|
||||
|
@ -268,6 +298,21 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
liveSortBy () {
|
||||
this.isElementListLoading = true
|
||||
this.latestLive = []
|
||||
switch (this.apiUsed) {
|
||||
case 'local':
|
||||
this.getChannelLiveLocal()
|
||||
break
|
||||
case 'invidious':
|
||||
this.channelInvidiousLive(true)
|
||||
break
|
||||
default:
|
||||
this.getChannelLiveLocal()
|
||||
}
|
||||
},
|
||||
|
||||
playlistSortBy () {
|
||||
this.isElementListLoading = true
|
||||
this.latestPlaylists = []
|
||||
|
@ -293,7 +338,14 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
this.id = this.$route.params.id
|
||||
this.currentTab = this.$route.params.currentTab ?? 'videos'
|
||||
|
||||
let currentTab = this.$route.params.currentTab ?? 'videos'
|
||||
|
||||
if (this.hideLiveStreams && currentTab === 'live') {
|
||||
currentTab = 'videos'
|
||||
}
|
||||
|
||||
this.currentTab = currentTab
|
||||
|
||||
if (this.id === '@@@') {
|
||||
this.showShareMenu = false
|
||||
|
@ -504,6 +556,10 @@ export default defineComponent({
|
|||
this.getChannelVideosLocal()
|
||||
}
|
||||
|
||||
if (!this.hideLiveStreams && channel.has_live_streams) {
|
||||
this.getChannelLiveLocal()
|
||||
}
|
||||
|
||||
if (channel.has_playlists) {
|
||||
this.getChannelPlaylistsLocal()
|
||||
}
|
||||
|
@ -573,7 +629,7 @@ export default defineComponent({
|
|||
let videosTab = await channel.getVideos()
|
||||
|
||||
if (this.videoSortBy !== 'newest') {
|
||||
const index = this.videoSelectValues.indexOf(this.videoSortBy)
|
||||
const index = this.videoShortLiveSelectValues.indexOf(this.videoSortBy)
|
||||
videosTab = await videosTab.applyFilter(videosTab.filters[index])
|
||||
}
|
||||
|
||||
|
@ -617,6 +673,62 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
|
||||
getChannelLiveLocal: async function () {
|
||||
this.isElementListLoading = true
|
||||
const expectedId = this.id
|
||||
|
||||
try {
|
||||
/**
|
||||
* @type {import('youtubei.js/dist/src/parser/youtube/Channel').default}
|
||||
*/
|
||||
const channel = this.channelInstance
|
||||
let liveTab = await channel.getLiveStreams()
|
||||
|
||||
if (this.liveSortBy !== 'newest') {
|
||||
const index = this.videoShortLiveSelectValues.indexOf(this.liveSortBy)
|
||||
liveTab = await liveTab.applyFilter(liveTab.filters[index])
|
||||
}
|
||||
|
||||
if (expectedId !== this.id) {
|
||||
return
|
||||
}
|
||||
|
||||
this.latestLive = parseLocalChannelVideos(liveTab.videos, channel.header.author)
|
||||
this.liveContinuationData = liveTab.has_continuation ? liveTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
if (this.backendPreference === 'local' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Invidious API'))
|
||||
this.getChannelInfoInvidious()
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getChannelLiveLocalMore: async function () {
|
||||
try {
|
||||
/**
|
||||
* @type {import('youtubei.js/dist/src/parser/youtube/Channel').ChannelListContinuation|import('youtubei.js/dist/src/parser/youtube/Channel').FilteredChannelList}
|
||||
*/
|
||||
const continuation = await this.liveContinuationData.getContinuation()
|
||||
|
||||
this.latestLive.push(...parseLocalChannelVideos(continuation.videos, this.channelInstance.header.author))
|
||||
this.liveContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
getChannelInfoInvidious: function () {
|
||||
this.isLoading = true
|
||||
this.apiUsed = 'invidious'
|
||||
|
@ -670,6 +782,10 @@ export default defineComponent({
|
|||
this.channelInvidiousVideos()
|
||||
}
|
||||
|
||||
if (!this.hideLiveStreams && response.tabs.includes('streams')) {
|
||||
this.channelInvidiousLive()
|
||||
}
|
||||
|
||||
if (response.tabs.includes('playlists')) {
|
||||
this.getPlaylistsInvidious()
|
||||
}
|
||||
|
@ -735,6 +851,47 @@ export default defineComponent({
|
|||
})
|
||||
},
|
||||
|
||||
channelInvidiousLive: function (sortByChanged) {
|
||||
const payload = {
|
||||
resource: 'channels',
|
||||
id: this.id,
|
||||
subResource: 'streams',
|
||||
params: {
|
||||
sort_by: this.liveSortBy,
|
||||
}
|
||||
}
|
||||
|
||||
if (sortByChanged) {
|
||||
this.liveContinuationData = null
|
||||
}
|
||||
|
||||
let more = false
|
||||
if (this.liveContinuationData) {
|
||||
payload.params.continuation = this.liveContinuationData
|
||||
more = true
|
||||
}
|
||||
|
||||
if (!more) {
|
||||
this.isElementListLoading = true
|
||||
}
|
||||
|
||||
invidiousAPICall(payload).then((response) => {
|
||||
if (more) {
|
||||
this.latestLive.push(...response.videos)
|
||||
} else {
|
||||
this.latestLive = response.videos
|
||||
}
|
||||
this.liveContinuationData = response.continuation || null
|
||||
this.isElementListLoading = false
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||
showToast(`${errorMessage}: ${err}`, 10000, () => {
|
||||
copyToClipboard(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
getChannelPlaylistsLocal: async function () {
|
||||
const expectedId = this.id
|
||||
|
||||
|
@ -1025,6 +1182,16 @@ export default defineComponent({
|
|||
break
|
||||
}
|
||||
break
|
||||
case 'live':
|
||||
switch (this.apiUsed) {
|
||||
case 'local':
|
||||
this.getChannelLiveLocalMore()
|
||||
break
|
||||
case 'invidious':
|
||||
this.channelInvidiousLive()
|
||||
break
|
||||
}
|
||||
break
|
||||
case 'playlists':
|
||||
switch (this.apiUsed) {
|
||||
case 'local':
|
||||
|
|
|
@ -100,6 +100,20 @@
|
|||
>
|
||||
{{ $t("Channel.Videos.Videos").toUpperCase() }}
|
||||
</div>
|
||||
<div
|
||||
v-if="!hideLiveStreams"
|
||||
id="liveTab"
|
||||
class="tab"
|
||||
:class="(currentTab==='live')?'selectedTab':''"
|
||||
role="tab"
|
||||
aria-selected="true"
|
||||
aria-controls="livePanel"
|
||||
tabindex="0"
|
||||
@click="changeTab('live')"
|
||||
@keydown.left.right.enter.space="changeTab('live', $event)"
|
||||
>
|
||||
{{ $t("Channel.Live.Live").toUpperCase() }}
|
||||
</div>
|
||||
<div
|
||||
id="playlistsTab"
|
||||
class="tab"
|
||||
|
@ -265,12 +279,22 @@
|
|||
<ft-select
|
||||
v-show="currentTab === 'videos' && latestVideos.length > 0"
|
||||
class="sortSelect"
|
||||
:value="videoSelectValues[0]"
|
||||
:select-names="videoSelectNames"
|
||||
:select-values="videoSelectValues"
|
||||
:value="videoShortLiveSelectValues[0]"
|
||||
:select-names="videoShortLiveSelectNames"
|
||||
:select-values="videoShortLiveSelectValues"
|
||||
:placeholder="$t('Search Filters.Sort By.Sort By')"
|
||||
@change="videoSortBy = $event"
|
||||
/>
|
||||
<ft-select
|
||||
v-if="!hideLiveStreams"
|
||||
v-show="currentTab === 'live' && latestLive.length > 0"
|
||||
class="sortSelect"
|
||||
:value="videoShortLiveSelectValues[0]"
|
||||
:select-names="videoShortLiveSelectNames"
|
||||
:select-values="videoShortLiveSelectValues"
|
||||
:placeholder="$t('Search Filters.Sort By.Sort By')"
|
||||
@change="liveSortBy = $event"
|
||||
/>
|
||||
<ft-select
|
||||
v-show="currentTab === 'playlists' && latestPlaylists.length > 0"
|
||||
class="sortSelect"
|
||||
|
@ -301,6 +325,21 @@
|
|||
{{ $t("Channel.Videos.This channel does not currently have any videos") }}
|
||||
</p>
|
||||
</ft-flex-box>
|
||||
<ft-element-list
|
||||
v-if="!hideLiveStreams"
|
||||
v-show="currentTab === 'live'"
|
||||
id="livePanel"
|
||||
:data="latestLive"
|
||||
role="tabpanel"
|
||||
aria-labelledby="liveTab"
|
||||
/>
|
||||
<ft-flex-box
|
||||
v-if="!hideLiveStreams && currentTab === 'live' && latestLive.length === 0"
|
||||
>
|
||||
<p class="message">
|
||||
{{ $t("Channel.Live.This channel does not currently have any live streams") }}
|
||||
</p>
|
||||
</ft-flex-box>
|
||||
<ft-element-list
|
||||
v-show="currentTab === 'playlists'"
|
||||
id="playlistPanel"
|
||||
|
|
|
@ -528,6 +528,10 @@ Channel:
|
|||
Newest: Newest
|
||||
Oldest: Oldest
|
||||
Most Popular: Most Popular
|
||||
Live:
|
||||
Live: Live
|
||||
This channel does not currently have any live streams: This channel does not currently
|
||||
have any live streams
|
||||
Playlists:
|
||||
Playlists: Playlists
|
||||
This channel does not currently have any playlists: This channel does not currently
|
||||
|
|
Loading…
Reference in New Issue