forked from platypush/platypush
New media webplugin WIP
This commit is contained in:
parent
f046752710
commit
ecd41a1f41
11 changed files with 158 additions and 18 deletions
|
@ -11,6 +11,12 @@
|
||||||
.item {
|
.item {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: $dropdown-disabled-color;
|
||||||
|
cursor: initial;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin: 0 .75rem;
|
margin: 0 .75rem;
|
||||||
|
|
|
@ -96,6 +96,7 @@ $header-bottom: $default-bottom;
|
||||||
|
|
||||||
//// Dropdown element
|
//// Dropdown element
|
||||||
$dropdown-bg: rgba(241,243,242,0.9) !default;
|
$dropdown-bg: rgba(241,243,242,0.9) !default;
|
||||||
|
$dropdown-disabled-color: #999 !default;
|
||||||
$dropdown-shadow: 1px 1px 1px #bbb !default;
|
$dropdown-shadow: 1px 1px 1px #bbb !default;
|
||||||
|
|
||||||
//// Modal element
|
//// Modal element
|
||||||
|
|
|
@ -20,13 +20,18 @@
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-left: 2rem;
|
margin-left: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-bottom: $default-border-3;
|
||||||
|
color: $devices-dropdown-refresh-fg;
|
||||||
|
font-size: .8em;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $hover-bg
|
background: $hover-bg
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,7 @@
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-item: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
|
|
|
@ -10,4 +10,5 @@ $control-time-color: #666;
|
||||||
$empty-results-color: #506050;
|
$empty-results-color: #506050;
|
||||||
|
|
||||||
$devices-dropdown-z-index: 2;
|
$devices-dropdown-z-index: 2;
|
||||||
|
$devices-dropdown-refresh-fg: #666;
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ Vue.component('dropdown', {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
clicked: function(item) {
|
clicked: function(item) {
|
||||||
if (item.click) {
|
if (item.click && !item.disabled) {
|
||||||
item.click();
|
item.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@ var clickHndl = function(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var element = event.target;
|
var element = event.target;
|
||||||
|
|
||||||
while (element) {
|
while (element) {
|
||||||
if (element == openedDropdown) {
|
if (element == openedDropdown) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Will be filled by dynamically loading device scripts
|
||||||
|
var mediaPlayers = {};
|
||||||
|
|
||||||
Vue.component('media-devices', {
|
Vue.component('media-devices', {
|
||||||
template: '#tmpl-media-devices',
|
template: '#tmpl-media-devices',
|
||||||
props: {
|
props: {
|
||||||
|
@ -9,12 +12,19 @@ Vue.component('media-devices', {
|
||||||
return {
|
return {
|
||||||
showDevicesMenu: false,
|
showDevicesMenu: false,
|
||||||
selectedDevice: {},
|
selectedDevice: {},
|
||||||
|
loading: false,
|
||||||
|
devices: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
dropdownItems: function() {
|
staticItems: function() {
|
||||||
var items = [
|
return [
|
||||||
|
{
|
||||||
|
text: 'Refresh',
|
||||||
|
type: 'refresh',
|
||||||
|
icon: 'sync-alt',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: this.playerPlugin,
|
name: this.playerPlugin,
|
||||||
text: this.playerPlugin,
|
text: this.playerPlugin,
|
||||||
|
@ -28,16 +38,42 @@ Vue.component('media-devices', {
|
||||||
icon: 'laptop',
|
icon: 'laptop',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
dropdownItems: function() {
|
||||||
|
const items = this.staticItems.concat(
|
||||||
|
this.devices.map(dev => {
|
||||||
|
return {
|
||||||
|
name: dev.name,
|
||||||
|
text: dev.name,
|
||||||
|
type: dev.__type__,
|
||||||
|
icon: dev.icon,
|
||||||
|
iconClass: dev.iconClass,
|
||||||
|
device: dev,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
const onClick = (item) => {
|
const onClick = (item) => {
|
||||||
return () => {
|
return () => {
|
||||||
|
if (self.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.selectDevice(item);
|
self.selectDevice(item);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var i=0; i < items.length; i++) {
|
for (var i=0; i < items.length; i++) {
|
||||||
items[i].click = onClick(items[i]);
|
if (items[i].type === 'refresh') {
|
||||||
|
items[i].click = this.refreshDevices;
|
||||||
|
} else {
|
||||||
|
items[i].click = onClick(items[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
items[i].disabled = this.loading;
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
@ -45,6 +81,48 @@ Vue.component('media-devices', {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
refreshDevices: async function() {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
var devices;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const promises = Object.entries(mediaPlayers).map((p) => {
|
||||||
|
const player = p[0];
|
||||||
|
const handler = p[1];
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
handler.scan().then(devs => {
|
||||||
|
for (var i=0; i < devs.length; i++) {
|
||||||
|
devs[i].__type__ = player;
|
||||||
|
|
||||||
|
if (handler.icon) {
|
||||||
|
devs[i].icon = handler.icon instanceof Function ? handler.icon(devs[i]) : handler.icon;
|
||||||
|
} else if (handler.iconClass) {
|
||||||
|
devs[i].iconClass = handler.iconClass instanceof Function ? handler.iconClass(devs[i]) : handler.iconClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(devs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.devices = (await Promise.all(promises)).reduce((list, devs) => {
|
||||||
|
for (const d of devs) {
|
||||||
|
list.push(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}, []);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
selectDevice: function(device) {
|
selectDevice: function(device) {
|
||||||
this.selectedDevice = device;
|
this.selectedDevice = device;
|
||||||
this.bus.$emit('selected-device', device);
|
this.bus.$emit('selected-device', device);
|
||||||
|
@ -57,6 +135,7 @@ Vue.component('media-devices', {
|
||||||
|
|
||||||
created: function() {
|
created: function() {
|
||||||
this.selectDevice(this.dropdownItems.filter(_ => _.type === 'local')[0]);
|
this.selectDevice(this.dropdownItems.filter(_ => _.type === 'local')[0]);
|
||||||
|
this.refreshDevices();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
mediaPlayers.chromecast = {
|
||||||
|
iconClass: function(item) {
|
||||||
|
if (item.type === 'audio') {
|
||||||
|
return 'fa fa-volume-up';
|
||||||
|
} else {
|
||||||
|
return 'fab fa-chromecast';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scan: async function() {
|
||||||
|
return await request('media.chromecast.get_chromecasts');
|
||||||
|
},
|
||||||
|
|
||||||
|
status: function(device) {
|
||||||
|
},
|
||||||
|
|
||||||
|
play: function(item) {
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: function() {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script type="text/x-template" id="tmpl-dropdown">
|
<script type="text/x-template" id="tmpl-dropdown">
|
||||||
<div class="dropdown" :id="id" :class="{hidden: !visible}">
|
<div class="dropdown" :id="id" :class="{hidden: !visible}">
|
||||||
<div class="row item" v-for="item in items" @click="clicked(item)">
|
<div class="row item" :class="{disabled: item.disabled}" v-for="item in items" @click="clicked(item)">
|
||||||
<div class="col-1 icon">
|
<div class="col-1 icon">
|
||||||
<i class="fa" :class="['fa-' + (item.icon || '')]" v-if="item.icon"></i>
|
<i class="fa" :class="['fa-' + (item.icon || '')]" v-if="item.icon"></i>
|
||||||
|
<i :class="item.iconClass" v-else-if="item.iconClass"></i>
|
||||||
<img src="item.img" v-else-if="item.image">
|
<img src="item.img" v-else-if="item.image">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-11 text" v-text="item.text"></div>
|
<div class="col-11 text" v-text="item.text"></div>
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/devices.js') }}"></script>
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/devices.js') }}"></script>
|
||||||
|
|
||||||
|
{% for script in utils.search_directory(static_folder + '/js/plugins/media/players', 'js', recursive=True) %}
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/players/' + script) }}"></script>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<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"
|
<button type="button"
|
||||||
class="devices"
|
class="devices"
|
||||||
:class="{selected: selectedDevice.type !== 'local'}"
|
:class="{selected: selectedDevice.type !== 'local' && selectedDevice.type !== 'browser'}"
|
||||||
:title="'Play on ' + (selectedDevice.name || '')"
|
:title="'Play on ' + (selectedDevice.name || '')"
|
||||||
@click="openDevicesMenu">
|
@click="openDevicesMenu">
|
||||||
<i class="fa" :class="'fa-' + (selectedDevice.icon || '')"></i>
|
<i class="fa" :class="'fa-' + (selectedDevice.icon || '')" v-if="selectedDevice.icon"></i>
|
||||||
|
<i :class="selectedDevice.iconClass" v-else-if="selectedDevice.iconClass"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<dropdown ref="menu" :items="dropdownItems">
|
<dropdown ref="menu" :items="dropdownItems">
|
||||||
|
|
|
@ -45,14 +45,35 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_chromecasts(self):
|
def get_chromecasts(self, tries=2, retry_wait=10, timeout=60,
|
||||||
|
blocking=True, callback=None):
|
||||||
"""
|
"""
|
||||||
Get the list of Chromecast devices
|
Get the list of Chromecast devices
|
||||||
|
|
||||||
|
:param tries: Number of retries (default: 2)
|
||||||
|
:type tries: int
|
||||||
|
|
||||||
|
:param retry_wait: Number of seconds between retries (default: 10 seconds)
|
||||||
|
:type retry_wait: int
|
||||||
|
|
||||||
|
:param timeout: Timeout before failing the call (default: 60 seconds)
|
||||||
|
:type timeout: int
|
||||||
|
|
||||||
|
:param blocking: If true, then the function will block until all the Chromecast
|
||||||
|
devices have been scanned. If false, then the provided callback function will be
|
||||||
|
invoked when a new device is discovered
|
||||||
|
:type blocking: bool
|
||||||
|
|
||||||
|
:param callback: If blocking is false, then you can provide a callback function that
|
||||||
|
will be invoked when a new device is discovered
|
||||||
|
:type callback: func
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.chromecasts.update({
|
self.chromecasts.update({
|
||||||
cast.device.friendly_name: cast
|
cast.device.friendly_name: cast
|
||||||
for cast in pychromecast.get_chromecasts()
|
for cast in pychromecast.get_chromecasts(tries=tries, retry_wait=retry_wait,
|
||||||
|
timeout=timeout, blocking=blocking,
|
||||||
|
callback=callback)
|
||||||
})
|
})
|
||||||
|
|
||||||
return [ {
|
return [ {
|
||||||
|
@ -81,7 +102,7 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
} for cc in self.chromecasts.values() ]
|
} for cc in self.chromecasts.values() ]
|
||||||
|
|
||||||
|
|
||||||
def get_chromecast(self, chromecast=None, n_tries=3):
|
def get_chromecast(self, chromecast=None, n_tries=2):
|
||||||
if isinstance(chromecast, pychromecast.Chromecast):
|
if isinstance(chromecast, pychromecast.Chromecast):
|
||||||
return chromecast
|
return chromecast
|
||||||
|
|
||||||
|
@ -307,8 +328,8 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
@action
|
@action
|
||||||
def disable_subtitles(self, chromecast=None, track_id=None):
|
def disable_subtitles(self, chromecast=None, track_id=None):
|
||||||
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
||||||
if track_name:
|
if track_id:
|
||||||
return mc.disable_subtitle(track_name)
|
return mc.disable_subtitle(track_id)
|
||||||
elif mc.current_subtitle_tracks:
|
elif mc.current_subtitle_tracks:
|
||||||
return mc.disable_subtitle(mc.current_subtitle_tracks[0])
|
return mc.disable_subtitle(mc.current_subtitle_tracks[0])
|
||||||
|
|
||||||
|
@ -319,9 +340,9 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
cur_subs = mc.status.status.current_subtitle_tracks
|
cur_subs = mc.status.status.current_subtitle_tracks
|
||||||
|
|
||||||
if cur_subs:
|
if cur_subs:
|
||||||
return self.disable_subtitle(chromecast, cur_subs[0])
|
return self.disable_subtitles(chromecast, cur_subs[0])
|
||||||
else:
|
else:
|
||||||
return self.enable_subtitle(chromecast, all_subs[0].get('trackId'))
|
return self.enable_subtitles(chromecast, all_subs[0].get('trackId'))
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
Loading…
Reference in a new issue