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-font-size: 1.5rem !default;
$default-shadow: 2px 2px 2px #ccc !default; $default-shadow: 2px 2px 2px #ccc !default;
$default-hover-fg: #35b870 !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-font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default;
$default-border: 1px solid #e1e4e8 !default; $default-border: 1px solid #e1e4e8 !default;

View file

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

View file

@ -15,6 +15,24 @@
height: inherit; height: inherit;
letter-spacing: .03rem; 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] { input[type=text] {
width: 100%; width: 100%;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,26 @@
mediaHandlers.file = { mediaHandlers.file = {
icon: 'hdd', iconClass: 'fa fa-hdd',
matchesUrl: function(url) { actions: [
return url.startsWith('file:///') || url.startsWith('/'); {
}, 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 = { mediaHandlers.torrent = {
icon: 'magnet', iconClass: 'fa fa-magnet',
matchesUrl: function(url) { actions: [
return url.startsWith('magnet:?') || url.endsWith('.torrent'); {
}, 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 = { mediaHandlers.youtube = {
icon: 'youtube', iconClass: 'fab fa-youtube',
matchesUrl: function(url) { actions: [
return url.startsWith('https://youtube.com/watch?v=') || {
url.startsWith('https://www.youtube.com/watch?v=') || text: 'Play',
url.startsWith('https://youtu.be/'); 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({}), bus: new Vue({}),
results: [], results: [],
currentItem: {}, currentItem: {},
selectedDevice: undefined,
loading: { loading: {
results: false, results: false,
media: false,
}, },
}; };
}, },
@ -33,22 +35,31 @@ Vue.component('media', {
this.loading.results = false; this.loading.results = false;
for (var i=0; i < results.length; i++) { for (var i=0; i < results.length; i++) {
results[i].handler = {}; results[i].handler = mediaHandlers[results[i].type];
for (const hndl of Object.values(mediaHandlers)) {
if (hndl.matchesUrl(results[i].url)) {
results[i].handler = hndl;
}
}
} }
this.results = results; this.results = results;
}, },
play: async function(item) {
},
info: function(item) {
// TODO
console.log(item);
},
selectDevice: function(device) {
this.selectedDevice = device;
},
}, },
created: function() { created: function() {
this.refresh(); 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-loading', this.onResultsLoading);
this.bus.$on('results-ready', this.onResultsReady); this.bus.$on('results-ready', this.onResultsReady);
}, },

View file

@ -2,10 +2,27 @@ Vue.component('media-item', {
template: '#tmpl-media-item', template: '#tmpl-media-item',
props: { props: {
bus: { type: Object }, bus: { type: Object },
selected: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: false,
},
item: { item: {
type: Object, type: Object,
default: () => {}, 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', template: '#tmpl-media-results',
props: { props: {
bus: { type: Object }, bus: { type: Object },
searching: {
type: Boolean,
default: false,
},
loading: { loading: {
type: Boolean, type: Boolean,
default: false, 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: { 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: { props: {
bus: { type: Object }, bus: { type: Object },
supportedTypes: { type: Object }, supportedTypes: { type: Object },
playerPlugin: { type: String },
}, },
data: function() { data: function() {

View file

@ -2,8 +2,12 @@
<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" title="Select target player" @click="openDevicesMenu"> <button type="button"
<i class="fa fa-podcast"></i> class="devices"
:class="{selected: selectedDevice.type !== 'local'}"
:title="'Play on ' + (selectedDevice.name || '')"
@click="openDevicesMenu">
<i class="fa" :class="'fa-' + (selectedDevice.icon || '')"></i>
</button> </button>
<dropdown ref="menu" :items="dropdownItems"> <dropdown ref="menu" :items="dropdownItems">

View file

@ -10,11 +10,14 @@
<script type="text/x-template" id="tmpl-media"> <script type="text/x-template" id="tmpl-media">
<div class="plugin media-plugin"> <div class="plugin media-plugin">
<media-search :bus="bus" <media-search :bus="bus"
:playerPlugin="player"
:supportedTypes="types"> :supportedTypes="types">
</media-search> </media-search>
<media-results :bus="bus" <media-results :bus="bus"
:loading="loading.results" :currentItem="currentItem"
:searching="loading.results"
:loading="loading.media"
:results="results"> :results="results">
</media-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="application/javascript" src="{{ url_for('static', filename='js/plugins/media/item.js') }}"></script>
<script type="text/x-template" id="tmpl-media-item"> <script type="text/x-template" id="tmpl-media-item">
<div class="media-item"> <div class="media-item"
<i :class="'fa fa-' + item.handler.icon" v-if="item.handler.length">&nbsp; </i> :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> <span v-text="item.title"></span>
</div> </div>
</script> </script>

View file

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

View file

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

View file

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