New media webplugin WIP

This commit is contained in:
Fabio Manganiello 2019-06-16 21:45:21 +02:00
parent 5e2b927267
commit e5d7334662
20 changed files with 250 additions and 32 deletions

View file

@ -10,6 +10,7 @@ $default-fg-3: #888888 !default;
$default-font-size: 1.5rem !default;
$default-shadow: 2px 2px 2px #ccc !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-border: 1px solid #e1e4e8 !default;

View file

@ -4,6 +4,16 @@
button {
padding: .5rem;
margin-right: .5rem;
&.selected {
background: initial;
color: $default-hover-fg;
&:hover {
color: $default-hover-fg-2;
}
}
}
.dropdown {
@ -12,6 +22,11 @@
align-items: center;
cursor: pointer;
.text {
text-align: left;
margin-left: 2rem;
}
&:hover {
background: $hover-bg
}

View file

@ -15,6 +15,24 @@
height: inherit;
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] {
width: 100%;

View file

@ -1,6 +1,7 @@
.media-plugin {
.results {
@include calc(height, '100% - 16rem');
position: relative; // For the dropdown menu
overflow: auto;
.empty {

View file

@ -9,3 +9,5 @@ $control-time-color: #666;
$empty-results-color: #506050;
$devices-dropdown-z-index: 2;

View file

@ -5,7 +5,7 @@ Vue.component('media-controls', {
item: {
type: Object,
default: () => {},
}
},
},
methods: {

View file

@ -2,11 +2,13 @@ Vue.component('media-devices', {
template: '#tmpl-media-devices',
props: {
bus: { type: Object },
playerPlugin: { type: String },
},
data: function() {
return {
showDevicesMenu: false,
selectedDevice: {},
};
},
@ -14,26 +16,47 @@ Vue.component('media-devices', {
dropdownItems: function() {
var items = [
{
text: 'Local player',
name: this.playerPlugin,
text: this.playerPlugin,
type: 'local',
icon: 'desktop',
},
{
name: 'browser',
text: 'Browser',
type: 'browser',
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;
},
},
methods: {
selectDevice: function(device) {
this.selectedDevice = device;
this.bus.$emit('selected-device', device);
},
openDevicesMenu: function() {
openDropdown(this.$refs.menu);
},
},
created: function() {
this.selectDevice(this.dropdownItems.filter(_ => _.type === 'local')[0]);
},
});

View file

@ -1,8 +1,26 @@
mediaHandlers.file = {
icon: 'hdd',
iconClass: 'fa fa-hdd',
matchesUrl: function(url) {
return url.startsWith('file:///') || url.startsWith('/');
},
actions: [
{
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',
},
],
};

View file

@ -1,8 +1,25 @@
mediaHandlers.torrent = {
icon: 'magnet',
iconClass: 'fa fa-magnet',
matchesUrl: function(url) {
return url.startsWith('magnet:?') || url.endsWith('.torrent');
},
actions: [
{
text: 'Play',
icon: 'play',
action: 'play',
},
{
text: 'Download',
icon: 'download',
action: function(item) {
},
},
{
text: 'View info',
icon: 'info',
action: 'info',
},
],
};

View file

@ -1,10 +1,25 @@
mediaHandlers.youtube = {
icon: 'youtube',
iconClass: 'fab fa-youtube',
matchesUrl: function(url) {
return url.startsWith('https://youtube.com/watch?v=') ||
url.startsWith('https://www.youtube.com/watch?v=') ||
url.startsWith('https://youtu.be/');
},
actions: [
{
text: 'Play',
icon: 'play',
action: 'play',
},
{
text: 'Download',
icon: 'download',
action: function(item) {
},
},
{
text: 'View info',
icon: 'info',
action: 'info',
},
],
};

View file

@ -9,8 +9,10 @@ Vue.component('media', {
bus: new Vue({}),
results: [],
currentItem: {},
selectedDevice: undefined,
loading: {
results: false,
media: false,
},
};
},
@ -33,22 +35,31 @@ Vue.component('media', {
this.loading.results = false;
for (var i=0; i < results.length; i++) {
results[i].handler = {};
for (const hndl of Object.values(mediaHandlers)) {
if (hndl.matchesUrl(results[i].url)) {
results[i].handler = hndl;
}
}
results[i].handler = mediaHandlers[results[i].type];
}
this.results = results;
},
play: async function(item) {
},
info: function(item) {
// TODO
console.log(item);
},
selectDevice: function(device) {
this.selectedDevice = device;
},
},
created: function() {
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-ready', this.onResultsReady);
},

View file

@ -2,10 +2,27 @@ Vue.component('media-item', {
template: '#tmpl-media-item',
props: {
bus: { type: Object },
selected: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: false,
},
item: {
type: Object,
default: () => {},
}
},
methods: {
onClick: function(event) {
this.bus.$emit('result-clicked', this.item);
},
},
});

View file

@ -2,6 +2,10 @@ Vue.component('media-results', {
template: '#tmpl-media-results',
props: {
bus: { type: Object },
searching: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
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: {
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);
},
});

View file

@ -3,6 +3,7 @@ Vue.component('media-search', {
props: {
bus: { type: Object },
supportedTypes: { type: Object },
playerPlugin: { type: String },
},
data: function() {

View file

@ -2,8 +2,12 @@
<script type="text/x-template" id="tmpl-media-devices">
<div class="devices">
<button type="button" title="Select target player" @click="openDevicesMenu">
<i class="fa fa-podcast"></i>
<button type="button"
class="devices"
:class="{selected: selectedDevice.type !== 'local'}"
:title="'Play on ' + (selectedDevice.name || '')"
@click="openDevicesMenu">
<i class="fa" :class="'fa-' + (selectedDevice.icon || '')"></i>
</button>
<dropdown ref="menu" :items="dropdownItems">

View file

@ -10,11 +10,14 @@
<script type="text/x-template" id="tmpl-media">
<div class="plugin media-plugin">
<media-search :bus="bus"
:playerPlugin="player"
:supportedTypes="types">
</media-search>
<media-results :bus="bus"
:loading="loading.results"
:currentItem="currentItem"
:searching="loading.results"
:loading="loading.media"
:results="results">
</media-results>

View file

@ -1,8 +1,10 @@
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/item.js') }}"></script>
<script type="text/x-template" id="tmpl-media-item">
<div class="media-item">
<i :class="'fa fa-' + item.handler.icon" v-if="item.handler.length">&nbsp; </i>
<div class="media-item"
:class="{selected: selected, active: active}"
@click="onClick">
<i :class="item.handler.iconClass" v-if="Object.keys(item.handler).length">&nbsp; </i>
<span v-text="item.title"></span>
</div>
</script>

View file

@ -2,17 +2,25 @@
<script type="text/x-template" id="tmpl-media-results">
<div class="results">
<div class="empty" v-if="loading || !results.length">
<div class="loading" v-if="loading">Loading</div>
<div class="empty" v-if="searching || loading || !results.length">
<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>
<media-item v-for="item in results"
:key="item.url"
:bus="bus"
:selected="Object.keys(selectedItem).length > 0 && item.url === selectedItem.url"
:active="Object.keys(currentItem).length > 0 && item.url === currentItem.url"
:item="item"
v-else>
</media-item>
<dropdown id="media-item-dropdown"
ref="mediaItemDropdown"
:items="mediaItemDropdownItems">
</dropdown>
</div>
</script>

View file

@ -20,7 +20,10 @@
</div>
<div class="col-1 pull-right">
<media-devices></media-devices>
<media-devices
:bus="bus"
:playerPlugin="playerPlugin">
</media-devices>
</div>
</div>

View file

@ -176,7 +176,7 @@ class MediaPlugin(Plugin):
if self._is_playing_torrent:
try:
get_plugin('media.webtorrent').quit()
except:
except Exception as e:
self.logger.warning('Cannot quit the webtorrent instance: {}'.
format(str(e)))
@ -305,9 +305,13 @@ class MediaPlugin(Plugin):
format(query, media_type))
flattened_results = []
for media_type in self._supported_media_types:
if media_type in results:
for result in results[media_type]:
result['type'] = media_type
flattened_results += results[media_type]
results = flattened_results
if results: