forked from platypush/platypush
[#337] Initial YouTube UI with feed support.
This commit is contained in:
parent
f425e95e7e
commit
96e69811fe
5 changed files with 276 additions and 3 deletions
|
@ -0,0 +1,109 @@
|
||||||
|
<template>
|
||||||
|
<div class="media-youtube-browser">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
|
||||||
|
<div class="browser" v-else>
|
||||||
|
<MediaNav :path="computedPath" @back="$emit('back')" />
|
||||||
|
<NoToken v-if="!authToken" />
|
||||||
|
|
||||||
|
<div class="body" v-else>
|
||||||
|
<Feed @play="$emit('play', $event)" v-if="selectedView === 'feed'" />
|
||||||
|
<Index @select="selectView" v-else />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import MediaNav from "./Nav";
|
||||||
|
import MediaProvider from "./Mixin";
|
||||||
|
|
||||||
|
import Feed from "./YouTube/Feed";
|
||||||
|
import Index from "./YouTube/Index";
|
||||||
|
import NoToken from "./YouTube/NoToken";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [MediaProvider],
|
||||||
|
components: {
|
||||||
|
Feed,
|
||||||
|
Index,
|
||||||
|
Loading,
|
||||||
|
MediaNav,
|
||||||
|
NoToken,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
youtubeConfig: null,
|
||||||
|
selectedView: null,
|
||||||
|
path: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
authToken() {
|
||||||
|
return this.youtubeConfig?.auth_token
|
||||||
|
},
|
||||||
|
|
||||||
|
computedPath() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'YouTube',
|
||||||
|
click: () => this.selectView(null),
|
||||||
|
icon: {
|
||||||
|
class: 'fab fa-youtube',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...this.path,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadYoutubeConfig() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
this.youtubeConfig = (await this.request('config.get_plugins')).youtube
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectView(view) {
|
||||||
|
this.selectedView = view
|
||||||
|
if (view?.length) {
|
||||||
|
this.path = [
|
||||||
|
{
|
||||||
|
title: view.slice(0, 1).toUpperCase() + view.slice(1),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
this.path = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadYoutubeConfig()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../style.scss";
|
||||||
|
|
||||||
|
.media-youtube-browser {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.browser {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
height: calc(100% - $media-nav-height - 2px);
|
||||||
|
margin-top: 2px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div class="media-youtube-feed">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<NoItems :with-shadow="false" v-else-if="!feed?.length">
|
||||||
|
No videos found.
|
||||||
|
</NoItems>
|
||||||
|
|
||||||
|
<Results :results="feed"
|
||||||
|
:sources="{'youtube': true}"
|
||||||
|
:selected-result="selectedResult"
|
||||||
|
@select="selectedResult = $event"
|
||||||
|
@play="$emit('play', $event)"
|
||||||
|
v-else />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NoItems from "@/components/elements/NoItems";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import Results from "@/components/panels/Media/Results";
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
emits: ['play'],
|
||||||
|
mixins: [Utils],
|
||||||
|
components: {
|
||||||
|
Loading,
|
||||||
|
NoItems,
|
||||||
|
Results,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
feed: [],
|
||||||
|
loading: false,
|
||||||
|
selectedResult: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadFeed() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
this.feed = (await this.request('youtube.get_feed')).map(item => ({
|
||||||
|
...item,
|
||||||
|
type: 'youtube',
|
||||||
|
}))
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.loadFeed()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.media-youtube-feed {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="youtube-views-browser grid">
|
||||||
|
<div class="item" @click="$emit('select', 'feed')">
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fas fa-rss" />
|
||||||
|
</div>
|
||||||
|
<div class="name">Feed</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item" @click="$emit('select', 'playlists')">
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fas fa-list" />
|
||||||
|
</div>
|
||||||
|
<div class="name">Playlists</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item" @click="$emit('select', 'subscriptions')">
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fas fa-user" />
|
||||||
|
</div>
|
||||||
|
<div class="name">Subscriptions</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
emits: ['select'],
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div class="no-token">
|
||||||
|
<div class="title">
|
||||||
|
No <code>auth_token</code> found in the YouTube configuration.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description">
|
||||||
|
This integration requires an <code>auth_token</code> to be set in the
|
||||||
|
<code>youtube</code> section of the configuration file in order to
|
||||||
|
access your playlists and subscriptions.<br/><br/>
|
||||||
|
|
||||||
|
Piped auth tokens are currently supported. You can retrieve one through
|
||||||
|
the following procedure:
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Login to your configured Piped instance.</li>
|
||||||
|
<li>Copy the RSS/Atom feed URL on the <i>Feed</i> tab.</li>
|
||||||
|
<li>Copy the <code>auth_token</code> query parameter from the URL.</li>
|
||||||
|
<li>
|
||||||
|
Enter it in the <code>auth_token</code> field in the
|
||||||
|
<code>youtube</code> section of the configuration file.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.no-token {
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="media-results">
|
<div class="media-results" @scroll="onScroll">
|
||||||
<Loading v-if="loading" />
|
<Loading v-if="loading" />
|
||||||
<NoItems v-else-if="!results?.length" :with-shadow="false">
|
<NoItems v-else-if="!results?.length" :with-shadow="false">
|
||||||
No search results
|
No search results
|
||||||
</NoItems>
|
</NoItems>
|
||||||
|
|
||||||
<div class="grid" v-else>
|
<div class="grid" ref="grid" v-else>
|
||||||
<Item v-for="(item, i) in results"
|
<Item v-for="(item, i) in visibleResults"
|
||||||
:key="i"
|
:key="i"
|
||||||
:item="item"
|
:item="item"
|
||||||
:selected="selectedResult === i"
|
:selected="selectedResult === i"
|
||||||
|
@ -54,6 +54,37 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resultIndexStep: {
|
||||||
|
type: Number,
|
||||||
|
default: 25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
maxResultIndex: this.resultIndexStep,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
visibleResults() {
|
||||||
|
return this.results.slice(0, this.maxResultIndex)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onScroll(e) {
|
||||||
|
const el = e.target
|
||||||
|
if (!el)
|
||||||
|
return
|
||||||
|
|
||||||
|
const bottom = (el.scrollHeight - el.scrollTop) <= el.clientHeight + 150
|
||||||
|
if (!bottom)
|
||||||
|
return
|
||||||
|
|
||||||
|
this.maxResultIndex += this.resultIndexStep
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
Loading…
Reference in a new issue