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