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 {
margin: 0 !important;
padding: 1rem;
cursor: pointer;
&.disabled {
color: $dropdown-disabled-color;
cursor: initial;
}
.icon {
margin: 0 .75rem;

View file

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

View file

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

View file

@ -20,8 +20,7 @@
.item {
display: flex;
align-item: center;
cursor: pointer;
align-items: center;
.text {
margin-left: 1rem;

View file

@ -10,4 +10,5 @@ $control-time-color: #666;
$empty-results-color: #506050;
$devices-dropdown-z-index: 2;
$devices-dropdown-refresh-fg: #666;

View file

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

View file

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

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

View file

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

View file

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