[YouTube UI] Added support for browsing playlists from search results.

This commit is contained in:
Fabio Manganiello 2024-07-12 03:10:43 +02:00
parent 7c610413df
commit e65bf99baf
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
8 changed files with 310 additions and 98 deletions

View file

@ -23,8 +23,9 @@
<component
:is="mediaProvider"
:filter="filter"
:selected-playlist="selectedPlaylist"
@add-to-playlist="$emit('add-to-playlist', $event)"
@back="mediaProvider = null"
@back="back"
@path-change="$emit('path-change', $event)"
@play="$emit('play', $event)"
/>
@ -44,6 +45,7 @@ export default {
mixins: [Utils],
emits: [
'add-to-playlist',
'back',
'create-playlist',
'path-change',
'play',
@ -62,6 +64,10 @@ export default {
type: String,
default: '',
},
selectedPlaylist: {
type: Object,
},
},
data() {
@ -73,7 +79,22 @@ export default {
}
},
computed: {
mediaProvidersLookup() {
return Object.keys(this.mediaProviders)
.reduce((acc, key) => {
acc[key.toLowerCase()] = key
return acc
}, {})
},
},
methods: {
back() {
this.mediaProvider = null
this.$emit('back')
},
registerMediaProvider(type) {
const component = shallowRef(
defineAsyncComponent(
@ -94,10 +115,29 @@ export default {
if (config.youtube)
this.registerMediaProvider('YouTube')
},
async onPlaylistChange() {
if (!this.selectedPlaylist)
return
const playlistType = this.selectedPlaylist.type?.toLowerCase()
const playlistMediaProvider = this.mediaProvidersLookup[playlistType]
if (playlistMediaProvider) {
this.mediaProvider = this.mediaProviders[playlistMediaProvider]
}
},
},
mounted() {
this.refreshMediaProviders()
watch: {
selectedPlaylist() {
this.onPlaylistChange()
},
},
async mounted() {
await this.refreshMediaProviders()
await this.onPlaylistChange()
},
}
</script>

View file

@ -8,7 +8,7 @@
<div class="nav-container from tablet" :style="navContainerStyle">
<Nav :selected-view="selectedView"
:torrent-plugin="torrentPlugin"
@input="selectedView = $event"
@input="onNavInput"
@toggle="forceShowNav = !forceShowNav" />
</div>
@ -52,7 +52,9 @@
v-else-if="selectedView === 'torrents'" />
<Browser :filter="browserFilter"
:selected-playlist="selectedPlaylist"
@add-to-playlist="addToPlaylistItem = $event"
@back="selectedResult = null"
@path-change="browserFilter = ''"
@play="play($event)"
v-else-if="selectedView === 'browser'" />
@ -195,6 +197,17 @@ export default {
return this.results[this.selectedResult]
},
selectedPlaylist() {
if (this.selectedResult == null)
return null
const selectedItem = this.results[this.selectedResult]
if (selectedItem?.item_type !== 'playlist')
return null
return this.results[this.selectedResult]
},
},
methods: {
@ -396,17 +409,22 @@ export default {
this.selectedResult = null
}
if (this.selectedResult != null && this.results[this.selectedResult]?.item_type === 'playlist') {
if (this.prevSelectedView != this.selectedView) {
this.prevSelectedView = this.selectedView
}
this.selectedView = 'browser'
const selectedItem = this.results[this.selectedResult]
if (this.selectedResult != null && selectedItem?.item_type === 'playlist') {
this.onPlaylistSelect()
} else {
this.selectedView = this.prevSelectedView || 'search'
}
},
onPlaylistSelect() {
if (this.prevSelectedView != this.selectedView) {
this.prevSelectedView = this.selectedView
}
this.selectedView = 'browser'
},
showPlayUrlModal() {
this.$refs.playUrlModal.show()
},
@ -425,6 +443,13 @@ export default {
this.loading = false
}
},
onNavInput(event) {
this.selectedView = event
if (event === 'search') {
this.selectedResult = null
}
},
},
mounted() {

View file

@ -19,6 +19,10 @@ export default {
type: String,
default: '',
},
selectedPlaylist: {
default: null,
},
},
data() {

View file

@ -14,7 +14,7 @@
/>
<Playlists :filter="filter"
:selected-playlist="selectedPlaylist"
:selected-playlist="selectedPlaylist_"
@add-to-playlist="$emit('add-to-playlist', $event)"
@play="$emit('play', $event)"
@remove-from-playlist="removeFromPlaylist"
@ -62,7 +62,7 @@ export default {
return {
youtubeConfig: null,
selectedView: null,
selectedPlaylist: null,
selectedPlaylist_: null,
selectedChannel: null,
path: [],
}
@ -124,7 +124,7 @@ export default {
selectView(view) {
this.selectedView = view
if (view === 'playlists')
this.selectedPlaylist = null
this.selectedPlaylist_ = null
else if (view === 'subscriptions')
this.selectedChannel = null
@ -141,7 +141,11 @@ export default {
},
onPlaylistSelected(playlist) {
this.selectedPlaylist = playlist.id
this.selectedPlaylist_ = playlist
if (!playlist)
return
this.selectedView = 'playlists'
this.path.push({
title: playlist.name,
})
@ -155,8 +159,15 @@ export default {
},
},
watch: {
selectedPlaylist() {
this.onPlaylistSelected(this.selectedPlaylist)
},
},
mounted() {
this.loadYoutubeConfig()
this.onPlaylistSelected(this.selectedPlaylist)
},
}
</script>

View file

@ -8,18 +8,23 @@
<img :src="channel.banner" v-if="channel?.banner?.length" />
</div>
<div class="row">
<a :href="channel.url" target="_blank" rel="noopener noreferrer">
<div class="image">
<img :src="channel.image" v-if="channel?.image?.length" />
</div>
</a>
<div class="row info-container">
<div class="info">
<a class="title" :href="channel.url" target="_blank" rel="noopener noreferrer">
{{ channel?.name }}
</a>
<div class="description">{{ channel?.description }}</div>
<div class="row">
<a :href="channel.url" target="_blank" rel="noopener noreferrer" v-if="channel?.image?.length">
<div class="image">
<img :src="channel.image" />
</div>
</a>
<a class="title" :href="channel.url" target="_blank" rel="noopener noreferrer">
{{ channel?.name }}
</a>
</div>
<div class="description" v-if="channel?.description">
{{ channel.description }}
</div>
</div>
</div>
</div>
@ -125,64 +130,14 @@ export default {
</script>
<style lang="scss" scoped>
@import "header.scss";
.media-youtube-channel {
height: 100%;
overflow-y: auto;
.header {
border-bottom: $default-border-2;
padding-bottom: 0.5em;
.banner {
max-height: 200px;
display: flex;
justify-content: center;
img {
max-width: 100%;
max-height: 100%;
}
}
.image {
height: 100px;
margin: -2.5em 2em 0.5em 0.5em;
img {
height: 100%;
border-radius: 50%;
}
}
.row {
display: flex;
@include from($desktop) {
flex-direction: row;
}
.info {
display: flex;
flex-direction: column;
}
}
.title {
color: $default-fg-2;
font-size: 1.7em;
font-weight: bold;
margin: 0.5em 0;
text-decoration: dotted;
&:hover {
color: $default-hover-fg;
}
}
.description {
font-size: 0.9em;
margin-right: 0.5em;
}
.channel {
height: 100%;
}
}
</style>

View file

@ -1,20 +1,60 @@
<template>
<div class="media-youtube-playlist">
<Loading v-if="loading" />
<NoItems :with-shadow="false" v-else-if="!items?.length">
No videos found.
</NoItems>
<Results :results="items"
:sources="{'youtube': true}"
:filter="filter"
:playlist="id"
:selected-result="selectedResult"
@add-to-playlist="$emit('add-to-playlist', $event)"
@play="$emit('play', $event)"
@remove-from-playlist="$emit('remove-from-playlist', $event)"
@select="selectedResult = $event"
v-else />
<div class="playlist-container" v-else>
<div class="header">
<div class="banner">
<img :src="metadata?.image" v-if="metadata?.image?.length" />
</div>
<div class="row info-container">
<div class="info">
<div class="row">
<a class="title" :href="metadata?.url" target="_blank" rel="noopener noreferrer" v-if="metadata?.url">
{{ name }}
</a>
<span class="title" v-else>
{{ name }}
</span>
<div class="n-items">{{ nItems }} videos</div>
</div>
<div class="row" v-if="metadata?.description">
<div class="description">
{{ metadata?.description }}
</div>
</div>
<div class="row" v-if="metadata?.channel_url">
<div class="channel">
Uploaded by
<a :href="metadata.channel_url" target="_blank" rel="noopener noreferrer">
{{ metadata?.channel }}
</a>
</div>
</div>
</div>
</div>
</div>
<NoItems :with-shadow="false" v-if="!nItems">
No videos found.
</NoItems>
<Results :results="items"
:sources="{'youtube': true}"
:filter="filter"
:playlist="id"
:selected-result="selectedResult"
@add-to-playlist="$emit('add-to-playlist', $event)"
@play="$emit('play', $event)"
@remove-from-playlist="$emit('remove-from-playlist', $event)"
@select="selectedResult = $event"
v-else />
</div>
</div>
</template>
@ -49,7 +89,7 @@ export default {
default: null,
},
playlist: {
metadata: {
type: Object,
default: null,
},
@ -63,6 +103,16 @@ export default {
}
},
computed: {
name() {
return this.metadata?.title || this.metadata?.name
},
nItems() {
return this.metadata?.videos || this.items?.length || 0
},
},
methods: {
async loadItems() {
this.loading = true
@ -86,7 +136,25 @@ export default {
</script>
<style lang="scss" scoped>
@import "header.scss";
.media-youtube-playlist {
height: 100%;
.playlist-container {
height: 100%;
display: flex;
flex-direction: column;
}
.header {
.banner {
opacity: 0.75;
}
.channel {
flex: 1;
}
}
}
</style>

View file

@ -1,6 +1,6 @@
<template>
<div class="media-youtube-playlists">
<div class="playlists-index" v-if="!selectedPlaylist">
<div class="playlists-index" v-if="!selectedPlaylist?.id">
<Loading v-if="loading" />
<NoItems :with-shadow="false" v-else-if="!playlists?.length">
No playlists found.
@ -27,11 +27,11 @@
<div class="playlist-body" v-else>
<Playlist
:id="selectedPlaylist"
:id="selectedPlaylist.id"
:filter="filter"
:playlist="playlist"
:metadata="playlistsById[selectedPlaylist.id] || selectedPlaylist"
@add-to-playlist="$emit('add-to-playlist', $event)"
@remove-from-playlist="$emit('remove-from-playlist', {item: $event, playlist_id: selectedPlaylist})"
@remove-from-playlist="$emit('remove-from-playlist', {item: $event, playlist_id: selectedPlaylist.id})"
@play="$emit('play', $event)"
/>
</div>
@ -130,7 +130,7 @@ export default {
props: {
selectedPlaylist: {
type: String,
type: Object,
default: null,
},

View file

@ -0,0 +1,109 @@
$banner-height: 100px;
$info-bg: rgba(0, 0, 0, 0.5);
$info-fg: rgba(255, 255, 255, 0.9);
.header {
border-bottom: $default-border-2;
margin-bottom: 0.5em;
position: relative;
overflow-y: auto;
.banner {
height: $banner-height;
display: flex;
background-color: black;
justify-content: center;
img {
max-width: 800px;
max-height: 100%;
flex: 1;
}
}
.image {
height: 100px;
margin: -2.5em 2em 0.5em 0.5em;
img {
height: 100%;
border-radius: 50%;
}
}
.row {
display: flex;
@include from($desktop) {
flex-direction: row;
}
.info {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
background-color: $info-bg;
.row {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
padding: 0 0.5em;
}
}
}
.info-container {
position: absolute;
top: 0;
width: 100%;
height: 100%;
background-color: $info-bg;
color: $info-fg;
display: flex;
flex-direction: column;
a {
color: $info-fg !important;
&:hover {
color: $default-hover-fg !important;
}
}
.title {
letter-spacing: 0.1em;
color: $info-fg !important;
}
.n-items {
/* Align to the right */
margin-left: auto;
padding: 0 0.5em;
}
}
.title {
color: $default-fg-2;
font-size: 1.7em;
font-weight: bold;
margin: 0.5em 0;
text-decoration: dotted;
&:hover {
color: $default-hover-fg;
}
}
.description {
font-size: 0.9em;
margin-right: 0.5em;
}
}
.media-results {
height: calc(100% - #{$banner-height} - 1em);
}