New media webplugin WIP

This commit is contained in:
Fabio Manganiello 2019-06-18 18:14:24 +02:00
parent f046752710
commit ecd41a1f41
11 changed files with 158 additions and 18 deletions

View file

@ -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;

View file

@ -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

View file

@ -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
} }

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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,23 +38,91 @@ 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++) {
if (items[i].type === 'refresh') {
items[i].click = this.refreshDevices;
} else {
items[i].click = onClick(items[i]); items[i].click = onClick(items[i]);
} }
items[i].disabled = this.loading;
}
return items; return items;
}, },
}, },
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();
}, },
}); });

View file

@ -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() {
},
};

View file

@ -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>

View file

@ -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">

View file

@ -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