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 {
|
||||
margin: 0 !important;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
color: $dropdown-disabled-color;
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 .75rem;
|
||||
|
|
|
@ -96,6 +96,7 @@ $header-bottom: $default-bottom;
|
|||
|
||||
//// Dropdown element
|
||||
$dropdown-bg: rgba(241,243,242,0.9) !default;
|
||||
$dropdown-disabled-color: #999 !default;
|
||||
$dropdown-shadow: 1px 1px 1px #bbb !default;
|
||||
|
||||
//// Modal element
|
||||
|
|
|
@ -20,13 +20,18 @@
|
|||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.text {
|
||||
text-align: left;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-bottom: $default-border-3;
|
||||
color: $devices-dropdown-refresh-fg;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $hover-bg
|
||||
}
|
||||
|
|
|
@ -20,8 +20,7 @@
|
|||
|
||||
.item {
|
||||
display: flex;
|
||||
align-item: center;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
margin-left: 1rem;
|
||||
|
|
|
@ -10,4 +10,5 @@ $control-time-color: #666;
|
|||
$empty-results-color: #506050;
|
||||
|
||||
$devices-dropdown-z-index: 2;
|
||||
$devices-dropdown-refresh-fg: #666;
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ Vue.component('dropdown', {
|
|||
|
||||
methods: {
|
||||
clicked: function(item) {
|
||||
if (item.click) {
|
||||
if (item.click && !item.disabled) {
|
||||
item.click();
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,6 @@ var clickHndl = function(event) {
|
|||
}
|
||||
|
||||
var element = event.target;
|
||||
|
||||
while (element) {
|
||||
if (element == openedDropdown) {
|
||||
return;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Will be filled by dynamically loading device scripts
|
||||
var mediaPlayers = {};
|
||||
|
||||
Vue.component('media-devices', {
|
||||
template: '#tmpl-media-devices',
|
||||
props: {
|
||||
|
@ -9,12 +12,19 @@ Vue.component('media-devices', {
|
|||
return {
|
||||
showDevicesMenu: false,
|
||||
selectedDevice: {},
|
||||
loading: false,
|
||||
devices: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
dropdownItems: function() {
|
||||
var items = [
|
||||
staticItems: function() {
|
||||
return [
|
||||
{
|
||||
text: 'Refresh',
|
||||
type: 'refresh',
|
||||
icon: 'sync-alt',
|
||||
},
|
||||
{
|
||||
name: this.playerPlugin,
|
||||
text: this.playerPlugin,
|
||||
|
@ -28,23 +38,91 @@ Vue.component('media-devices', {
|
|||
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 onClick = (item) => {
|
||||
return () => {
|
||||
if (self.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.selectDevice(item);
|
||||
};
|
||||
};
|
||||
|
||||
for (var i=0; i < items.length; 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;
|
||||
},
|
||||
},
|
||||
|
||||
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) {
|
||||
this.selectedDevice = device;
|
||||
this.bus.$emit('selected-device', device);
|
||||
|
@ -57,6 +135,7 @@ Vue.component('media-devices', {
|
|||
|
||||
created: function() {
|
||||
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">
|
||||
<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">
|
||||
<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">
|
||||
</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>
|
||||
|
||||
{% 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">
|
||||
<div class="devices">
|
||||
<button type="button"
|
||||
class="devices"
|
||||
:class="{selected: selectedDevice.type !== 'local'}"
|
||||
:class="{selected: selectedDevice.type !== 'local' && selectedDevice.type !== 'browser'}"
|
||||
:title="'Play on ' + (selectedDevice.name || '')"
|
||||
@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>
|
||||
|
||||
<dropdown ref="menu" :items="dropdownItems">
|
||||
|
|
|
@ -45,14 +45,35 @@ class MediaChromecastPlugin(MediaPlugin):
|
|||
|
||||
|
||||
@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
|
||||
|
||||
: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({
|
||||
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 [ {
|
||||
|
@ -81,7 +102,7 @@ class MediaChromecastPlugin(MediaPlugin):
|
|||
} 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):
|
||||
return chromecast
|
||||
|
||||
|
@ -307,8 +328,8 @@ class MediaChromecastPlugin(MediaPlugin):
|
|||
@action
|
||||
def disable_subtitles(self, chromecast=None, track_id=None):
|
||||
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
||||
if track_name:
|
||||
return mc.disable_subtitle(track_name)
|
||||
if track_id:
|
||||
return mc.disable_subtitle(track_id)
|
||||
elif mc.current_subtitle_tracks:
|
||||
return mc.disable_subtitle(mc.current_subtitle_tracks[0])
|
||||
|
||||
|
@ -319,9 +340,9 @@ class MediaChromecastPlugin(MediaPlugin):
|
|||
cur_subs = mc.status.status.current_subtitle_tracks
|
||||
|
||||
if cur_subs:
|
||||
return self.disable_subtitle(chromecast, cur_subs[0])
|
||||
return self.disable_subtitles(chromecast, cur_subs[0])
|
||||
else:
|
||||
return self.enable_subtitle(chromecast, all_subs[0].get('trackId'))
|
||||
return self.enable_subtitles(chromecast, all_subs[0].get('trackId'))
|
||||
|
||||
|
||||
@action
|
||||
|
|
Loading…
Reference in a new issue