forked from platypush/platypush
New media webplugin WIP
This commit is contained in:
parent
5e2b927267
commit
e5d7334662
20 changed files with 250 additions and 32 deletions
|
@ -10,6 +10,7 @@ $default-fg-3: #888888 !default;
|
|||
$default-font-size: 1.5rem !default;
|
||||
$default-shadow: 2px 2px 2px #ccc !default;
|
||||
$default-hover-fg: #35b870 !default;
|
||||
$default-hover-fg-2: #38cf80 !default;
|
||||
|
||||
$default-font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
|
||||
$default-border: 1px solid #e1e4e8 !default;
|
||||
|
|
|
@ -4,6 +4,16 @@
|
|||
|
||||
button {
|
||||
padding: .5rem;
|
||||
margin-right: .5rem;
|
||||
|
||||
&.selected {
|
||||
background: initial;
|
||||
color: $default-hover-fg;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
@ -12,6 +22,11 @@
|
|||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.text {
|
||||
text-align: left;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg
|
||||
}
|
||||
|
|
|
@ -15,6 +15,24 @@
|
|||
height: inherit;
|
||||
letter-spacing: .03rem;
|
||||
|
||||
.dropdown {
|
||||
z-index: $devices-dropdown-z-index;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-item: center;
|
||||
cursor: pointer;
|
||||
|
||||
.text {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
width: 100%;
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.media-plugin {
|
||||
.results {
|
||||
@include calc(height, '100% - 16rem');
|
||||
position: relative; // For the dropdown menu
|
||||
overflow: auto;
|
||||
|
||||
.empty {
|
||||
|
|
|
@ -9,3 +9,5 @@ $control-time-color: #666;
|
|||
|
||||
$empty-results-color: #506050;
|
||||
|
||||
$devices-dropdown-z-index: 2;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ Vue.component('media-controls', {
|
|||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
@ -2,11 +2,13 @@ Vue.component('media-devices', {
|
|||
template: '#tmpl-media-devices',
|
||||
props: {
|
||||
bus: { type: Object },
|
||||
playerPlugin: { type: String },
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
showDevicesMenu: false,
|
||||
selectedDevice: {},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -14,26 +16,47 @@ Vue.component('media-devices', {
|
|||
dropdownItems: function() {
|
||||
var items = [
|
||||
{
|
||||
text: 'Local player',
|
||||
name: this.playerPlugin,
|
||||
text: this.playerPlugin,
|
||||
type: 'local',
|
||||
icon: 'desktop',
|
||||
},
|
||||
{
|
||||
name: 'browser',
|
||||
text: 'Browser',
|
||||
type: 'browser',
|
||||
icon: 'laptop',
|
||||
},
|
||||
];
|
||||
|
||||
const self = this;
|
||||
const onClick = (item) => {
|
||||
return () => {
|
||||
self.selectDevice(item);
|
||||
};
|
||||
};
|
||||
|
||||
for (var i=0; i < items.length; i++) {
|
||||
items[i].click = onClick(items[i]);
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectDevice: function(device) {
|
||||
this.selectedDevice = device;
|
||||
this.bus.$emit('selected-device', device);
|
||||
},
|
||||
|
||||
openDevicesMenu: function() {
|
||||
openDropdown(this.$refs.menu);
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
this.selectDevice(this.dropdownItems.filter(_ => _.type === 'local')[0]);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,8 +1,26 @@
|
|||
mediaHandlers.file = {
|
||||
icon: 'hdd',
|
||||
iconClass: 'fa fa-hdd',
|
||||
|
||||
matchesUrl: function(url) {
|
||||
return url.startsWith('file:///') || url.startsWith('/');
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
text: 'Play',
|
||||
icon: 'play',
|
||||
action: 'play',
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Download',
|
||||
icon: 'download',
|
||||
action: function(item, bus) {
|
||||
bus.$emit('download', item);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'View info',
|
||||
icon: 'info',
|
||||
action: 'info',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,25 @@
|
|||
mediaHandlers.torrent = {
|
||||
icon: 'magnet',
|
||||
iconClass: 'fa fa-magnet',
|
||||
|
||||
matchesUrl: function(url) {
|
||||
return url.startsWith('magnet:?') || url.endsWith('.torrent');
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
text: 'Play',
|
||||
icon: 'play',
|
||||
action: 'play',
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Download',
|
||||
icon: 'download',
|
||||
action: function(item) {
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'View info',
|
||||
icon: 'info',
|
||||
action: 'info',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
mediaHandlers.youtube = {
|
||||
icon: 'youtube',
|
||||
iconClass: 'fab fa-youtube',
|
||||
|
||||
matchesUrl: function(url) {
|
||||
return url.startsWith('https://youtube.com/watch?v=') ||
|
||||
url.startsWith('https://www.youtube.com/watch?v=') ||
|
||||
url.startsWith('https://youtu.be/');
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
text: 'Play',
|
||||
icon: 'play',
|
||||
action: 'play',
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Download',
|
||||
icon: 'download',
|
||||
action: function(item) {
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'View info',
|
||||
icon: 'info',
|
||||
action: 'info',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -9,8 +9,10 @@ Vue.component('media', {
|
|||
bus: new Vue({}),
|
||||
results: [],
|
||||
currentItem: {},
|
||||
selectedDevice: undefined,
|
||||
loading: {
|
||||
results: false,
|
||||
media: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -33,22 +35,31 @@ Vue.component('media', {
|
|||
this.loading.results = false;
|
||||
|
||||
for (var i=0; i < results.length; i++) {
|
||||
results[i].handler = {};
|
||||
|
||||
for (const hndl of Object.values(mediaHandlers)) {
|
||||
if (hndl.matchesUrl(results[i].url)) {
|
||||
results[i].handler = hndl;
|
||||
}
|
||||
}
|
||||
results[i].handler = mediaHandlers[results[i].type];
|
||||
}
|
||||
|
||||
this.results = results;
|
||||
},
|
||||
|
||||
play: async function(item) {
|
||||
},
|
||||
|
||||
info: function(item) {
|
||||
// TODO
|
||||
console.log(item);
|
||||
},
|
||||
|
||||
selectDevice: function(device) {
|
||||
this.selectedDevice = device;
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
this.refresh();
|
||||
|
||||
this.bus.$on('play', this.play);
|
||||
this.bus.$on('info', this.info);
|
||||
this.bus.$on('selected-device', this.selectDevice);
|
||||
this.bus.$on('results-loading', this.onResultsLoading);
|
||||
this.bus.$on('results-ready', this.onResultsReady);
|
||||
},
|
||||
|
|
|
@ -2,10 +2,27 @@ Vue.component('media-item', {
|
|||
template: '#tmpl-media-item',
|
||||
props: {
|
||||
bus: { type: Object },
|
||||
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick: function(event) {
|
||||
this.bus.$emit('result-clicked', this.item);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -2,6 +2,10 @@ Vue.component('media-results', {
|
|||
template: '#tmpl-media-results',
|
||||
props: {
|
||||
bus: { type: Object },
|
||||
searching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -12,7 +16,58 @@ Vue.component('media-results', {
|
|||
},
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
selectedItem: {},
|
||||
currentItem: {},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
mediaItemDropdownItems: function() {
|
||||
if (!Object.keys(this.selectedItem).length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
return this.selectedItem.handler.actions.map(action => {
|
||||
return {
|
||||
text: action.text,
|
||||
icon: action.icon,
|
||||
click: function() {
|
||||
if (action.action instanceof Function) {
|
||||
action.action(self.selectedItem, self.bus);
|
||||
} else if (typeof(action.action) === 'string') {
|
||||
self[action.action](self.selectedItem);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
itemClicked: function(item) {
|
||||
if (this.selectedItem.length && this.selectedItem.url === item.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedItem = item;
|
||||
openDropdown(this.$refs.mediaItemDropdown);
|
||||
},
|
||||
|
||||
play: function(item) {
|
||||
this.bus.$emit('play', item);
|
||||
},
|
||||
|
||||
info: function(item) {
|
||||
this.bus.$emit('info', item);
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
this.bus.$on('result-clicked', this.itemClicked);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ Vue.component('media-search', {
|
|||
props: {
|
||||
bus: { type: Object },
|
||||
supportedTypes: { type: Object },
|
||||
playerPlugin: { type: String },
|
||||
},
|
||||
|
||||
data: function() {
|
||||
|
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
<script type="text/x-template" id="tmpl-media-devices">
|
||||
<div class="devices">
|
||||
<button type="button" title="Select target player" @click="openDevicesMenu">
|
||||
<i class="fa fa-podcast"></i>
|
||||
<button type="button"
|
||||
class="devices"
|
||||
:class="{selected: selectedDevice.type !== 'local'}"
|
||||
:title="'Play on ' + (selectedDevice.name || '')"
|
||||
@click="openDevicesMenu">
|
||||
<i class="fa" :class="'fa-' + (selectedDevice.icon || '')"></i>
|
||||
</button>
|
||||
|
||||
<dropdown ref="menu" :items="dropdownItems">
|
||||
|
|
|
@ -10,11 +10,14 @@
|
|||
<script type="text/x-template" id="tmpl-media">
|
||||
<div class="plugin media-plugin">
|
||||
<media-search :bus="bus"
|
||||
:playerPlugin="player"
|
||||
:supportedTypes="types">
|
||||
</media-search>
|
||||
|
||||
<media-results :bus="bus"
|
||||
:loading="loading.results"
|
||||
:currentItem="currentItem"
|
||||
:searching="loading.results"
|
||||
:loading="loading.media"
|
||||
:results="results">
|
||||
</media-results>
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/item.js') }}"></script>
|
||||
|
||||
<script type="text/x-template" id="tmpl-media-item">
|
||||
<div class="media-item">
|
||||
<i :class="'fa fa-' + item.handler.icon" v-if="item.handler.length"> </i>
|
||||
<div class="media-item"
|
||||
:class="{selected: selected, active: active}"
|
||||
@click="onClick">
|
||||
<i :class="item.handler.iconClass" v-if="Object.keys(item.handler).length"> </i>
|
||||
<span v-text="item.title"></span>
|
||||
</div>
|
||||
</script>
|
||||
|
|
|
@ -2,17 +2,25 @@
|
|||
|
||||
<script type="text/x-template" id="tmpl-media-results">
|
||||
<div class="results">
|
||||
<div class="empty" v-if="loading || !results.length">
|
||||
<div class="loading" v-if="loading">Loading</div>
|
||||
<div class="empty" v-if="searching || loading || !results.length">
|
||||
<div class="searching" v-if="searching">Searching</div>
|
||||
<div class="loading" v-else-if="loading">Loading</div>
|
||||
<div class="no-results" v-else-if="!results.length">No results</div>
|
||||
</div>
|
||||
|
||||
<media-item v-for="item in results"
|
||||
:key="item.url"
|
||||
:bus="bus"
|
||||
:selected="Object.keys(selectedItem).length > 0 && item.url === selectedItem.url"
|
||||
:active="Object.keys(currentItem).length > 0 && item.url === currentItem.url"
|
||||
:item="item"
|
||||
v-else>
|
||||
</media-item>
|
||||
|
||||
<dropdown id="media-item-dropdown"
|
||||
ref="mediaItemDropdown"
|
||||
:items="mediaItemDropdownItems">
|
||||
</dropdown>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
|
|
@ -20,7 +20,10 @@
|
|||
</div>
|
||||
|
||||
<div class="col-1 pull-right">
|
||||
<media-devices></media-devices>
|
||||
<media-devices
|
||||
:bus="bus"
|
||||
:playerPlugin="playerPlugin">
|
||||
</media-devices>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@ class MediaPlugin(Plugin):
|
|||
if self._is_playing_torrent:
|
||||
try:
|
||||
get_plugin('media.webtorrent').quit()
|
||||
except:
|
||||
except Exception as e:
|
||||
self.logger.warning('Cannot quit the webtorrent instance: {}'.
|
||||
format(str(e)))
|
||||
|
||||
|
@ -305,9 +305,13 @@ class MediaPlugin(Plugin):
|
|||
format(query, media_type))
|
||||
|
||||
flattened_results = []
|
||||
|
||||
for media_type in self._supported_media_types:
|
||||
if media_type in results:
|
||||
for result in results[media_type]:
|
||||
result['type'] = media_type
|
||||
flattened_results += results[media_type]
|
||||
|
||||
results = flattened_results
|
||||
|
||||
if results:
|
||||
|
|
Loading…
Reference in a new issue