diff --git a/platypush/backend/http/webapp/src/components/elements/ConfirmDialog.vue b/platypush/backend/http/webapp/src/components/elements/ConfirmDialog.vue index b1c82d2f8..ad9329985 100644 --- a/platypush/backend/http/webapp/src/components/elements/ConfirmDialog.vue +++ b/platypush/backend/http/webapp/src/components/elements/ConfirmDialog.vue @@ -43,13 +43,21 @@ export default { this.close() }, - show() { + open() { this.$refs.modal.show() }, close() { this.$refs.modal.hide() }, + + show() { + this.open() + }, + + hide() { + this.close() + }, }, } </script> diff --git a/platypush/backend/http/webapp/src/components/elements/TextPrompt.vue b/platypush/backend/http/webapp/src/components/elements/TextPrompt.vue index e68aee55b..3c0f5aa75 100644 --- a/platypush/backend/http/webapp/src/components/elements/TextPrompt.vue +++ b/platypush/backend/http/webapp/src/components/elements/TextPrompt.vue @@ -3,7 +3,7 @@ <form @submit.prevent="onConfirm"> <div class="dialog-content"> <slot /> - <input type="text" ref="input" /> + <input type="text" ref="input" v-model="value_" /> </div> <div class="buttons"> @@ -38,26 +38,79 @@ export default { type: String, default: "Cancel", }, + + visible: { + type: Boolean, + default: false, + }, + + value: { + type: String, + default: "", + }, + }, + + data() { + return { + value_: "", + visible_: false, + } }, methods: { onConfirm() { - this.$emit('input', this.$refs.input.value) + this.$emit('input', this.value_) this.close() }, - show() { + open() { + if (this.visible_) + return + + this.value_ = this.value this.$refs.modal.show() + this.visible_ = true + this.focus() }, close() { + if (!this.visible_) + return + + this.value_ = "" this.$refs.modal.hide() + this.visible_ = false + }, + + show() { + this.open() + }, + + hide() { + this.close() + }, + + focus() { + this.$nextTick(() => { + this.$refs.input.focus() + }) + }, + }, + + watch: { + visible(val) { + if (val) { + this.open() + } else { + this.close() + } }, }, mounted() { + this.visible_ = this.visible + this.value_ = this.value || "" this.$nextTick(() => { - this.$refs.input.value = "" this.$refs.input.focus() }) }, diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Browser.vue b/platypush/backend/http/webapp/src/components/panels/Media/Browser.vue index 5244b21ec..291cc989e 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Browser.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Browser.vue @@ -23,9 +23,11 @@ <component :is="mediaProvider" :filter="filter" + @add-to-playlist="$emit('add-to-playlist', $event)" @back="mediaProvider = null" @path-change="$emit('path-change', $event)" - @play="$emit('play', $event)" /> + @play="$emit('play', $event)" + /> </div> </div> </keep-alive> @@ -39,8 +41,17 @@ import Utils from "@/Utils"; import providersMetadata from "./Providers/meta.json"; export default { - emits: ['path-change', 'play'], mixins: [Utils], + emits: [ + 'add-to-playlist', + 'create-playlist', + 'path-change', + 'play', + 'remove-from-playlist', + 'remove-playlist', + 'rename-playlist', + ], + components: { Browser, Loading, diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Index.vue b/platypush/backend/http/webapp/src/components/panels/Media/Index.vue index 311d3bb18..364a330af 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Index.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Index.vue @@ -39,6 +39,7 @@ :plugin-name="pluginName" :loading="loading" :filter="browserFilter" + @add-to-playlist="addToPlaylistItem = $event" @select="onResultSelect($event)" @play="play" @view="view" @@ -51,6 +52,7 @@ v-else-if="selectedView === 'torrents'" /> <Browser :filter="browserFilter" + @add-to-playlist="addToPlaylistItem = $event" @path-change="browserFilter = ''" @play="play($event)" v-else-if="selectedView === 'browser'" /> @@ -75,6 +77,16 @@ <UrlPlayer :value="urlPlay" @input="urlPlay = $event.target.value" @play="playUrl($event)" /> </Modal> </div> + + <div class="add-to-playlist-container" v-if="addToPlaylistItem"> + <Modal title="Add to playlist" :visible="addToPlaylistItem != null" @close="addToPlaylistItem = null"> + <PlaylistAdder + :item="addToPlaylistItem" + @done="addToPlaylistItem = null" + @close="addToPlaylistItem = null" + /> + </Modal> + </div> </div> </keep-alive> </template> @@ -88,6 +100,7 @@ import Header from "@/components/panels/Media/Header"; import MediaUtils from "@/components/Media/Utils"; import MediaView from "@/components/Media/View"; import Nav from "@/components/panels/Media/Nav"; +import PlaylistAdder from "@/components/panels/Media/PlaylistAdder"; import Results from "@/components/panels/Media/Results"; import Subtitles from "@/components/panels/Media/Subtitles"; import Transfers from "@/components/panels/Torrent/Transfers"; @@ -102,6 +115,7 @@ export default { MediaView, Modal, Nav, + PlaylistAdder, Results, Subtitles, Transfers, @@ -139,6 +153,7 @@ export default { awaitingPlayTorrent: null, urlPlay: null, browserFilter: null, + addToPlaylistItem: null, torrentPlugin: null, torrentPlugins: [ 'torrent', @@ -487,4 +502,10 @@ export default { } } } + +:deep(.add-to-playlist-container) { + .body { + padding: 0 !important; + } +} </style> diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Item.vue b/platypush/backend/http/webapp/src/components/panels/Media/Item.vue index 902f46cae..0ffceb3e6 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Item.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Item.vue @@ -4,6 +4,7 @@ :class="{selected: selected}" @click.right.prevent="$refs.dropdown.toggle()" v-if="!hidden"> + <div class="thumbnail"> <MediaImage :item="item" @play="$emit('play')" /> </div> @@ -19,6 +20,10 @@ v-if="item.type === 'torrent'" /> <DropdownItem icon-class="fa fa-window-maximize" text="View in browser" @click="$emit('view')" v-if="item.type === 'file'" /> + <DropdownItem icon-class="fa fa-list" text="Add to playlist" @click="$emit('add-to-playlist')" + v-if="item.type === 'youtube'" /> + <DropdownItem icon-class="fa fa-trash" text="Remove from playlist" @click="$refs.confirmPlaylistRemove.open()" + v-if="playlist" /> <DropdownItem icon-class="fa fa-info-circle" text="Info" @click="$emit('select')" /> </Dropdown> </div> @@ -35,10 +40,15 @@ {{ formatDateTime(item.created_at, true) }} </div> </div> + + <ConfirmDialog ref="confirmPlaylistRemove" @input="$emit('remove-from-playlist')"> + Are you sure you want to remove this item from the playlist? + </ConfirmDialog> </div> </template> <script> +import ConfirmDialog from "@/components/elements/ConfirmDialog"; import Dropdown from "@/components/elements/Dropdown"; import DropdownItem from "@/components/elements/DropdownItem"; import Icons from "./icons.json"; @@ -46,9 +56,23 @@ import MediaImage from "./MediaImage"; import Utils from "@/Utils"; export default { - components: {Dropdown, DropdownItem, MediaImage}, mixins: [Utils], - emits: ['play', 'select', 'view', 'download'], + components: { + ConfirmDialog, + Dropdown, + DropdownItem, + MediaImage, + }, + + emits: [ + 'add-to-playlist', + 'download', + 'play', + 'remove-from-playlist', + 'select', + 'view', + ], + props: { item: { type: Object, @@ -64,6 +88,10 @@ export default { type: Boolean, default: false, }, + + playlist: { + default: null, + }, }, data() { @@ -87,6 +115,10 @@ export default { border: 1px solid transparent; border-bottom: 1px solid transparent !important; + @include from($tablet) { + max-height: max(25em, 25%); + } + &.selected { box-shadow: $border-shadow-bottom; background: $selected-bg; diff --git a/platypush/backend/http/webapp/src/components/panels/Media/PlaylistAdder.vue b/platypush/backend/http/webapp/src/components/panels/Media/PlaylistAdder.vue new file mode 100644 index 000000000..1bf1a25d2 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/panels/Media/PlaylistAdder.vue @@ -0,0 +1,162 @@ +<template> + <div class="playlist-adder-container"> + <Loading v-if="loading" /> + <TextPrompt ref="newPlaylistName" :visible="showNewPlaylist" @input="createPlaylist($event)"> + Playlist name + </TextPrompt> + + <div class="playlists"> + <div class="playlist new-playlist"> + <button @click="showNewPlaylist = true"> + <i class="fa fa-plus" /> + Create new playlist + </button> + </div> + + <div class="playlist" v-for="playlist in playlists" :key="playlist.id"> + <button @click="addToPlaylist(playlist.id)"> + <i class="fa fa-list" /> + {{ playlist.name }} + </button> + </div> + </div> + </div> +</template> + +<script> +import Loading from "@/components/Loading"; +import Utils from "@/Utils"; +import TextPrompt from "@/components/elements/TextPrompt" + +export default { + emits: ['done'], + mixins: [Utils], + components: {Loading, TextPrompt}, + props: { + item: { + type: Object, + required: true, + }, + }, + + data() { + return { + loading: false, + playlists: [], + showNewPlaylist: false, + } + }, + + methods: { + async createPlaylist(name) { + name = name?.trim() + if (!name?.length) + return + + this.loading = true + + try { + const playlist = await this.request('youtube.create_playlist', { + name: name, + }) + + await this.request('youtube.add_to_playlist', { + playlist_id: playlist.id, + video_id: this.item.id || this.item.url, + }) + + this.$emit('done') + this.notify({ + text: 'Playlist created and video added', + image: { + icon: 'check', + } + }) + + } finally { + this.loading = false + this.showNewPlaylist = false + } + }, + + async refreshPlaylists() { + this.loading = true + + try { + this.playlists = await this.request('youtube.get_playlists') + } finally { + this.loading = false + } + }, + + async addToPlaylist(playlistId) { + this.loading = true + + try { + await this.request('youtube.add_to_playlist', { + playlist_id: playlistId, + video_id: this.item.id || this.item.url, + }) + + this.notify({ + text: 'Video added to playlist', + image: { + icon: 'check', + } + }) + + this.$emit('done') + } finally { + this.loading = false + } + }, + }, + + mounted() { + this.refreshPlaylists() + }, +} +</script> + +<style lang="scss" scoped> +.playlist-adder-container { + min-width: 300px; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + + .playlists { + width: 100%; + overflow-y: auto; + } + + .playlist { + button { + width: 100%; + text-align: left; + padding: 0.5em 1em; + border: none; + background: none; + cursor: pointer; + transition: background 0.2s, color 0.2s; + + &:hover { + background: $hover-bg; + } + + i { + margin-right: 0.5em; + } + } + } + + .new-playlist { + button { + font-weight: bold; + border-bottom: $default-border; + } + } +} +</style> diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/Mixin.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/Mixin.vue index 8de115bc2..4d0181901 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/Mixin.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/Mixin.vue @@ -2,8 +2,18 @@ import Utils from "@/Utils"; export default { - emits: ['back', 'path-change', 'play'], mixins: [Utils], + emits: [ + 'add-to-playlist', + 'back', + 'create-playlist', + 'path-change', + 'play', + 'remove-from-playlist', + 'remove-playlist', + 'rename-playlist', + ], + props: { filter: { type: String, diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue index c6491590d..f2907cec4 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue @@ -8,17 +8,27 @@ <div class="body" v-else> <Feed :filter="filter" - @play="$emit('play', $event)" v-if="selectedView === 'feed'" /> + @add-to-playlist="$emit('add-to-playlist', $event)" + @play="$emit('play', $event)" + v-if="selectedView === 'feed'" + /> + <Playlists :filter="filter" :selected-playlist="selectedPlaylist" + @add-to-playlist="$emit('add-to-playlist', $event)" @play="$emit('play', $event)" + @remove-from-playlist="removeFromPlaylist" @select="onPlaylistSelected" - v-else-if="selectedView === 'playlists'" /> + v-else-if="selectedView === 'playlists'" + /> + <Subscriptions :filter="filter" :selected-channel="selectedChannel" @play="$emit('play', $event)" @select="onChannelSelected" - v-else-if="selectedView === 'subscriptions'" /> + v-else-if="selectedView === 'subscriptions'" + /> + <Index @select="selectView" v-else /> </div> </div> @@ -87,6 +97,21 @@ export default { } }, + async removeFromPlaylist(event) { + const playlistId = event.playlist_id + const videoId = event.item.url + this.loading = true + + try { + await this.request('youtube.remove_from_playlist', { + playlist_id: playlistId, + video_id: videoId, + }) + } finally { + this.loading = false + } + }, + selectView(view) { this.selectedView = view if (view === 'playlists') diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Feed.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Feed.vue index eaadfda6c..58793c622 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Feed.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Feed.vue @@ -9,6 +9,7 @@ :filter="filter" :sources="{'youtube': true}" :selected-result="selectedResult" + @add-to-playlist="$emit('add-to-playlist', $event)" @select="selectedResult = $event" @play="$emit('play', $event)" v-else /> @@ -22,8 +23,12 @@ import Results from "@/components/panels/Media/Results"; import Utils from "@/Utils"; export default { - emits: ['play'], mixins: [Utils], + emits: [ + 'add-to-playlist', + 'play', + ], + components: { Loading, NoItems, diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlist.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlist.vue index c09d0089a..4a70e42e0 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlist.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlist.vue @@ -8,9 +8,12 @@ <Results :results="items" :sources="{'youtube': true}" :filter="filter" + :playlist="id" :selected-result="selectedResult" - @select="selectedResult = $event" + @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> </template> @@ -22,8 +25,13 @@ import Results from "@/components/panels/Media/Results"; import Utils from "@/Utils"; export default { - emits: ['play'], mixins: [Utils], + emits: [ + 'add-to-playlist', + 'play', + 'remove-from-playlist', + ], + components: { Loading, NoItems, diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue index 823e11145..fae4433bf 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue @@ -18,7 +18,14 @@ </div> <div class="playlist-body" v-else> - <Playlist :id="selectedPlaylist" :filter="filter" @play="$emit('play', $event)" /> + <Playlist + :id="selectedPlaylist" + :filter="filter" + :playlist="playlist" + @add-to-playlist="$emit('add-to-playlist', $event)" + @remove-from-playlist="$emit('remove-from-playlist', {item: $event, playlist_id: selectedPlaylist})" + @play="$emit('play', $event)" + /> </div> </div> </template> @@ -31,8 +38,17 @@ import Playlist from "./Playlist"; import Utils from "@/Utils"; export default { - emits: ['play', 'select'], mixins: [Utils], + emits: [ + 'add-to-playlist', + 'create-playlist', + 'play', + 'remove-from-playlist', + 'remove-playlist', + 'rename-playlist', + 'select', + ], + components: { Loading, MediaImage, diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Results.vue b/platypush/backend/http/webapp/src/components/panels/Media/Results.vue index 62f14722b..5d4292f46 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Results.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Results.vue @@ -4,9 +4,12 @@ <div class="grid" ref="grid" v-if="results?.length" @scroll="onScroll"> <Item v-for="(item, i) in visibleResults" :key="i" - :item="item" - :selected="selectedResult === i" :hidden="!!Object.keys(sources || {}).length && !sources[item.type]" + :item="item" + :playlist="playlist" + :selected="selectedResult === i" + @add-to-playlist="$emit('add-to-playlist', item)" + @remove-from-playlist="$emit('remove-from-playlist', item)" @select="$emit('select', i)" @play="$emit('play', item)" @view="$emit('view', item)" @@ -30,7 +33,16 @@ import Modal from "@/components/Modal"; export default { components: {Info, Item, Loading, Modal}, - emits: ['select', 'play', 'view', 'download', 'scroll-end'], + emits: [ + 'add-to-playlist', + 'download', + 'play', + 'remove-from-playlist', + 'scroll-end', + 'select', + 'view', + ], + props: { loading: { type: Boolean, @@ -64,6 +76,10 @@ export default { type: Number, default: 25, }, + + playlist: { + default: null, + }, }, data() {