Migrating media plugins [WIP]

This commit is contained in:
Fabio Manganiello 2021-01-05 00:50:24 +01:00
parent 67d3b40772
commit 6ae76f1f38
67 changed files with 1294 additions and 87 deletions

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path d="M25.14 2c-.76 0-1.47.3-2 .84l-5.85 5.93a.969.969 0 0 0-.29.7v20.69c0 .4.25.77.62.92a1 1 0 0 0 .38.08c.26 0 .52-.11.71-.3l17.35-17.58a1 1 0 0 0 0-1.4l-8.93-9.04C26.6 2.3 25.89 2 25.14 2zM11.122 15.813a1 1 0 0 0-.8.257L2.9 22.96C2.32 23.5 2 24.22 2 25c0 .77.32 1.49.9 2.03l7.42 6.9c.19.17.43.27.68.27.14 0 .27-.03.4-.09.36-.16.6-.52.6-.91V16.8c0-.4-.24-.76-.6-.91a.982.982 0 0 0-.279-.078zm29.473.962c-.261 0-.523.096-.713.286l-8.592 8.68a.996.996 0 0 0 0 1.41l7.16 7.23c.19.19.451.299.711.299.27 0 .52-.11.71-.3l7.31-7.37a2.871 2.871 0 0 0 0-4.02l-5.88-5.93a.985.985 0 0 0-.706-.285zm-13.768 13.86a.985.985 0 0 0-.707.285l-8.83 8.91a.984.984 0 0 0 0 1.4l5.88 5.94c.54.53 1.25.83 2 .83.76 0 1.47-.3 2-.83l7.542-7.6a.996.996 0 0 0 0-1.41l-7.172-7.24a1.007 1.007 0 0 0-.713-.285z"/><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description about="https://iconscout.com/legal#licenses" dc:title="kodi,filled" dc:description="kodi,filled" dc:publisher="Iconscout" dc:date="2017-12-09" dc:format="image/svg+xml" dc:language="en"><dc:creator><rdf:Bag><rdf:li>Icons8</rdf:li></rdf:Bag></dc:creator></rdf:Description></rdf:RDF></metadata></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>platypush</title><link href="/static/css/chunk-24ff873d.64d9bc0b.css" rel="prefetch"><link href="/static/css/chunk-3b44ec4e.0c4a18da.css" rel="prefetch"><link href="/static/css/chunk-45939517.e4a1ddf3.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.3108d379.css" rel="prefetch"><link href="/static/css/chunk-4c0b0f48.009b6a70.css" rel="prefetch"><link href="/static/css/chunk-4eeb8349.2026dd4f.css" rel="prefetch"><link href="/static/css/chunk-53360c78.c486a396.css" rel="prefetch"><link href="/static/css/chunk-5fea187e.d6e3f8eb.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.6cb54f10.css" rel="prefetch"><link href="/static/css/chunk-d8561e02.b52f89a0.css" rel="prefetch"><link href="/static/css/chunk-e8078048.c6785c78.css" rel="prefetch"><link href="/static/js/chunk-24ff873d.691c883d.js" rel="prefetch"><link href="/static/js/chunk-2d2091df.1e51ae4c.js" rel="prefetch"><link href="/static/js/chunk-2d21da1a.adf909a2.js" rel="prefetch"><link href="/static/js/chunk-3b44ec4e.904c7e10.js" rel="prefetch"><link href="/static/js/chunk-45939517.c0034c6b.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.251fff37.js" rel="prefetch"><link href="/static/js/chunk-4c0b0f48.366980a2.js" rel="prefetch"><link href="/static/js/chunk-4eeb8349.5c94d58c.js" rel="prefetch"><link href="/static/js/chunk-53360c78.51ee7c96.js" rel="prefetch"><link href="/static/js/chunk-5fea187e.4466d92f.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.17d3c86d.js" rel="prefetch"><link href="/static/js/chunk-d8561e02.1e366cb3.js" rel="prefetch"><link href="/static/js/chunk-e8078048.ce29b8d4.js" rel="prefetch"><link href="/static/css/app.4868c461.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="preload" as="style"><link href="/static/js/app.3770dd06.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.30e3a6cb.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="stylesheet"><link href="/static/css/app.4868c461.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.30e3a6cb.js"></script><script src="/static/js/app.3770dd06.js"></script></body></html> <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>platypush</title><link href="/static/css/chunk-24ff873d.934c66a7.css" rel="prefetch"><link href="/static/css/chunk-3b44ec4e.120a35b6.css" rel="prefetch"><link href="/static/css/chunk-3ffdd2f0.63f19efc.css" rel="prefetch"><link href="/static/css/chunk-45939517.f01c29cc.css" rel="prefetch"><link href="/static/css/chunk-4bbbb9a3.1a24453a.css" rel="prefetch"><link href="/static/css/chunk-4c0b0f48.6b856cb1.css" rel="prefetch"><link href="/static/css/chunk-4eeb8349.804fa9fd.css" rel="prefetch"><link href="/static/css/chunk-53360c78.d5bec80e.css" rel="prefetch"><link href="/static/css/chunk-62a3d08e.a5b70794.css" rel="prefetch"><link href="/static/css/chunk-a60951ae.5863e0d4.css" rel="prefetch"><link href="/static/css/chunk-d8561e02.105050d2.css" rel="prefetch"><link href="/static/css/chunk-e8078048.9f279ae9.css" rel="prefetch"><link href="/static/css/chunk-fa962a0a.4d6f6ce2.css" rel="prefetch"><link href="/static/js/chunk-24ff873d.691c883d.js" rel="prefetch"><link href="/static/js/chunk-2d0b270c.795e924c.js" rel="prefetch"><link href="/static/js/chunk-2d0c1eb0.6a91d527.js" rel="prefetch"><link href="/static/js/chunk-2d2091df.1e51ae4c.js" rel="prefetch"><link href="/static/js/chunk-2d21b0dc.df7794a5.js" rel="prefetch"><link href="/static/js/chunk-2d21da1a.adf909a2.js" rel="prefetch"><link href="/static/js/chunk-2d231217.ec7b8ee5.js" rel="prefetch"><link href="/static/js/chunk-3b44ec4e.904c7e10.js" rel="prefetch"><link href="/static/js/chunk-3ffdd2f0.c56c2562.js" rel="prefetch"><link href="/static/js/chunk-45939517.c0034c6b.js" rel="prefetch"><link href="/static/js/chunk-4bbbb9a3.251fff37.js" rel="prefetch"><link href="/static/js/chunk-4c0b0f48.366980a2.js" rel="prefetch"><link href="/static/js/chunk-4eeb8349.5c94d58c.js" rel="prefetch"><link href="/static/js/chunk-53360c78.51ee7c96.js" rel="prefetch"><link href="/static/js/chunk-62a3d08e.17d3c86d.js" rel="prefetch"><link href="/static/js/chunk-a60951ae.8bfe85bc.js" rel="prefetch"><link href="/static/js/chunk-d8561e02.1e366cb3.js" rel="prefetch"><link href="/static/js/chunk-e8078048.ce29b8d4.js" rel="prefetch"><link href="/static/js/chunk-fa962a0a.314e65ea.js" rel="prefetch"><link href="/static/css/app.fd1d75f5.css" rel="preload" as="style"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="preload" as="style"><link href="/static/js/app.0247d3bf.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.ac361ae9.js" rel="preload" as="script"><link href="/static/css/chunk-vendors.5dad8b00.css" rel="stylesheet"><link href="/static/css/app.fd1d75f5.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.ac361ae9.js"></script><script src="/static/js/app.0247d3bf.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0b270c"],{"23b7":function(e,a,n){"use strict";n.r(a);var c=n("7a23"),d=Object(c["K"])("data-v-52effd7c"),t=d((function(e,a,n,d,t,i){var p=Object(c["z"])("Media");return Object(c["r"])(),Object(c["e"])(p,{"plugin-name":"media.mpv"})})),i=n("3951"),p={name:"MediaMpv",components:{Media:i["default"]}};p.render=t,p.__scopeId="data-v-52effd7c";a["default"]=p}}]);
//# sourceMappingURL=chunk-2d0b270c.795e924c.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/MediaMpv/Index.vue","webpack:///./src/components/panels/MediaMpv/Index.vue?ac2c"],"names":["plugin-name","name","components","Media","render","__scopeId"],"mappings":"8PACE,eAAiC,GAA1BA,cAAY,iB,YAMN,GACbC,KAAM,WACNC,WAAY,CAACC,MAAA,eCNf,EAAOC,OAAS,EAChB,EAAOC,UAAY,kBAEJ","file":"static/js/chunk-2d0b270c.795e924c.js","sourcesContent":["<template>\n <Media plugin-name=\"media.mpv\" />\n</template>\n\n<script>\nimport Media from '@/components/panels/Media/Index'\n\nexport default {\n name: \"MediaMpv\",\n components: {Media},\n}\n</script>\n\n<style scoped>\n\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=52effd7c&scoped=true&bindings={}\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\nscript.render = render\nscript.__scopeId = \"data-v-52effd7c\"\n\nexport default script"],"sourceRoot":""}

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0c1eb0"],{"47a8":function(e,a,n){"use strict";n.r(a);var t=n("7a23"),c=Object(t["K"])("data-v-08ab61b7"),d=c((function(e,a,n,c,d,b){var r=Object(t["z"])("Media");return Object(t["r"])(),Object(t["e"])(r,{"plugin-name":"media.mplayer"})})),b=n("3951"),r={name:"MediaMplayer",components:{Media:b["default"]}};r.render=d,r.__scopeId="data-v-08ab61b7";a["default"]=r}}]);
//# sourceMappingURL=chunk-2d0c1eb0.6a91d527.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/MediaMplayer/Index.vue","webpack:///./src/components/panels/MediaMplayer/Index.vue?9e10"],"names":["plugin-name","name","components","Media","render","__scopeId"],"mappings":"8PACE,eAAqC,GAA9BA,cAAY,qB,YAMN,GACbC,KAAM,eACNC,WAAY,CAACC,MAAA,eCNf,EAAOC,OAAS,EAChB,EAAOC,UAAY,kBAEJ","file":"static/js/chunk-2d0c1eb0.6a91d527.js","sourcesContent":["<template>\n <Media plugin-name=\"media.mplayer\" />\n</template>\n\n<script>\nimport Media from '@/components/panels/Media/Index'\n\nexport default {\n name: \"MediaMplayer\",\n components: {Media},\n}\n</script>\n\n<style scoped>\n\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=08ab61b7&scoped=true&bindings={}\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\nscript.render = render\nscript.__scopeId = \"data-v-08ab61b7\"\n\nexport default script"],"sourceRoot":""}

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d21b0dc"],{bdae:function(e,a,n){"use strict";n.r(a);var c=n("7a23"),d=Object(c["K"])("data-v-9233e214"),t=d((function(e,a,n,d,t,i){var o=Object(c["z"])("Media");return Object(c["r"])(),Object(c["e"])(o,{"plugin-name":"media.vlc"})})),i=n("3951"),o={name:"MediaVlc",components:{Media:i["default"]}};o.render=t,o.__scopeId="data-v-9233e214";a["default"]=o}}]);
//# sourceMappingURL=chunk-2d21b0dc.df7794a5.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/MediaVlc/Index.vue","webpack:///./src/components/panels/MediaVlc/Index.vue?e504"],"names":["plugin-name","name","components","Media","render","__scopeId"],"mappings":"4PACE,eAAiC,GAA1BA,cAAY,iB,YAMN,GACbC,KAAM,WACNC,WAAY,CAACC,MAAA,eCNf,EAAOC,OAAS,EAChB,EAAOC,UAAY,kBAEJ","file":"static/js/chunk-2d21b0dc.df7794a5.js","sourcesContent":["<template>\n <Media plugin-name=\"media.vlc\" />\n</template>\n\n<script>\nimport Media from '@/components/panels/Media/Index'\n\nexport default {\n name: \"MediaVlc\",\n components: {Media},\n}\n</script>\n\n<style scoped>\n\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=9233e214&scoped=true&bindings={}\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\nscript.render = render\nscript.__scopeId = \"data-v-9233e214\"\n\nexport default script"],"sourceRoot":""}

View file

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d231217"],{eede:function(e,a,d){"use strict";d.r(a);var n=d("7a23"),c=Object(n["K"])("data-v-7264d7fc"),t=c((function(e,a,d,c,t,i){var o=Object(n["z"])("Media");return Object(n["r"])(),Object(n["e"])(o,{"plugin-name":"media.omxplayer"})})),i=d("3951"),o={name:"MediaMpv",components:{Media:i["default"]}};o.render=t,o.__scopeId="data-v-7264d7fc";a["default"]=o}}]);
//# sourceMappingURL=chunk-2d231217.ec7b8ee5.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/panels/MediaOmxplayer/Index.vue","webpack:///./src/components/panels/MediaOmxplayer/Index.vue?207d"],"names":["plugin-name","name","components","Media","render","__scopeId"],"mappings":"4PACE,eAAuC,GAAhCA,cAAY,uB,YAMN,GACbC,KAAM,WACNC,WAAY,CAACC,MAAA,eCNf,EAAOC,OAAS,EAChB,EAAOC,UAAY,kBAEJ","file":"static/js/chunk-2d231217.ec7b8ee5.js","sourcesContent":["<template>\n <Media plugin-name=\"media.omxplayer\" />\n</template>\n\n<script>\nimport Media from '@/components/panels/Media/Index'\n\nexport default {\n name: \"MediaMpv\",\n components: {Media},\n}\n</script>\n\n<style scoped>\n\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=7264d7fc&scoped=true&bindings={}\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\nscript.render = render\nscript.__scopeId = \"data-v-7264d7fc\"\n\nexport default script"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path d="M25.14 2c-.76 0-1.47.3-2 .84l-5.85 5.93a.969.969 0 0 0-.29.7v20.69c0 .4.25.77.62.92a1 1 0 0 0 .38.08c.26 0 .52-.11.71-.3l17.35-17.58a1 1 0 0 0 0-1.4l-8.93-9.04C26.6 2.3 25.89 2 25.14 2zM11.122 15.813a1 1 0 0 0-.8.257L2.9 22.96C2.32 23.5 2 24.22 2 25c0 .77.32 1.49.9 2.03l7.42 6.9c.19.17.43.27.68.27.14 0 .27-.03.4-.09.36-.16.6-.52.6-.91V16.8c0-.4-.24-.76-.6-.91a.982.982 0 0 0-.279-.078zm29.473.962c-.261 0-.523.096-.713.286l-8.592 8.68a.996.996 0 0 0 0 1.41l7.16 7.23c.19.19.451.299.711.299.27 0 .52-.11.71-.3l7.31-7.37a2.871 2.871 0 0 0 0-4.02l-5.88-5.93a.985.985 0 0 0-.706-.285zm-13.768 13.86a.985.985 0 0 0-.707.285l-8.83 8.91a.984.984 0 0 0 0 1.4l5.88 5.94c.54.53 1.25.83 2 .83.76 0 1.47-.3 2-.83l7.542-7.6a.996.996 0 0 0 0-1.41l-7.172-7.24a1.007 1.007 0 0 0-.713-.285z"/><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description about="https://iconscout.com/legal#licenses" dc:title="kodi,filled" dc:description="kodi,filled" dc:publisher="Iconscout" dc:date="2017-12-09" dc:format="image/svg+xml" dc:language="en"><dc:creator><rdf:Bag><rdf:li>Icons8</rdf:li></rdf:Bag></dc:creator></rdf:Description></rdf:RDF></metadata></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -3,6 +3,18 @@
"light.hue": { "light.hue": {
"class": "fas fa-lightbulb" "class": "fas fa-lightbulb"
}, },
"media.omxplayer": {
"class": "fa fa-film"
},
"media.mplayer": {
"class": "fa fa-film"
},
"media.mpv": {
"class": "fa fa-film"
},
"media.vlc": {
"class": "fa fa-film"
},
"music.mpd": { "music.mpd": {
"class": "fas fa-music" "class": "fas fa-music"
} }

View file

@ -5,13 +5,13 @@
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="buttons"> <div class="buttons">
<button @click="$emit('previous')" title="Play previous track" v-if="buttons.previous"> <button @click="$emit('previous')" title="Play previous track" v-if="buttons_.previous">
<i class="icon fa fa-step-backward"></i> <i class="icon fa fa-step-backward"></i>
</button> </button>
<button @click="$emit('stop')" v-if="buttons.stop && status.state !== 'stop'" title="Stop playback"> <button @click="$emit('stop')" v-if="buttons_.stop && status.state !== 'stop'" title="Stop playback">
<i class="icon fa fa-stop"></i> <i class="icon fa fa-stop"></i>
</button> </button>
<button @click="$emit('next')" title="Play next track" v-if="buttons.next"> <button @click="$emit('next')" title="Play next track" v-if="buttons_.next">
<i class="icon fa fa-step-forward"></i> <i class="icon fa fa-step-forward"></i>
</button> </button>
</div> </div>
@ -35,17 +35,17 @@
<div class="col-3 list-controls"> <div class="col-3 list-controls">
<button @click="$emit('consume', !status.consume)" :class="{enabled: status.consume}" <button @click="$emit('consume', !status.consume)" :class="{enabled: status.consume}"
title="Toggle consume mode" v-if="buttons.consume"> title="Toggle consume mode" v-if="buttons_.consume">
<i class="icon fa fa-utensils"></i> <i class="icon fa fa-utensils"></i>
</button> </button>
<button @click="$emit('random', !status.random)" :class="{enabled: status.random}" <button @click="$emit('random', !status.random)" :class="{enabled: status.random}"
title="Toggle shuffle" v-if="buttons.random"> title="Toggle shuffle" v-if="buttons_.random">
<i class="icon fa fa-random"></i> <i class="icon fa fa-random"></i>
</button> </button>
<button @click="$emit('repeat', !status.repeat)" :class="{enabled: status.repeat}" <button @click="$emit('repeat', !status.repeat)" :class="{enabled: status.repeat}"
title="Toggle repeat" v-if="buttons.repeat"> title="Toggle repeat" v-if="buttons_.repeat">
<i class="icon fa fa-redo"></i> <i class="icon fa fa-redo"></i>
</button> </button>
</div> </div>
@ -81,6 +81,7 @@
<div class="title"> <div class="title">
<a :href="$route.fullPath" v-text="track.title" <a :href="$route.fullPath" v-text="track.title"
@click.prevent="$emit('search', {artist: track.artist, album: track.album})" v-if="track.album"></a> @click.prevent="$emit('search', {artist: track.artist, album: track.album})" v-if="track.album"></a>
<a :href="track.url" v-text="track.title" v-else-if="track.url"></a>
<span v-text="track.title" v-else></span> <span v-text="track.title" v-else></span>
</div> </div>
<div class="artist" v-if="track.artist"> <div class="artist" v-if="track.artist">
@ -91,7 +92,7 @@
<div class="playback-controls desktop col-6"> <div class="playback-controls desktop col-6">
<div class="row buttons"> <div class="row buttons">
<button @click="$emit('previous')" title="Play previous track" v-if="buttons.previous"> <button @click="$emit('previous')" title="Play previous track" v-if="buttons_.previous">
<i class="icon fa fa-step-backward"></i> <i class="icon fa fa-step-backward"></i>
</button> </button>
<button @click="$emit(status.state === 'play' ? 'pause' : 'play')" <button @click="$emit(status.state === 'play' ? 'pause' : 'play')"
@ -99,10 +100,10 @@
<i class="icon play-pause fa fa-pause" v-if="status.state === 'play'"></i> <i class="icon play-pause fa fa-pause" v-if="status.state === 'play'"></i>
<i class="icon play-pause fa fa-play" v-else></i> <i class="icon play-pause fa fa-play" v-else></i>
</button> </button>
<button @click="$emit('stop')" v-if="buttons.stop && status.state !== 'stop'" title="Stop playback"> <button @click="$emit('stop')" v-if="buttons_.stop && status.state !== 'stop'" title="Stop playback">
<i class="icon fa fa-stop"></i> <i class="icon fa fa-stop"></i>
</button> </button>
<button @click="$emit('next')" title="Play next track" v-if="buttons.next"> <button @click="$emit('next')" title="Play next track" v-if="buttons_.next">
<i class="icon fa fa-step-forward"></i> <i class="icon fa fa-step-forward"></i>
</button> </button>
</div> </div>
@ -131,13 +132,13 @@
<div class="col-3 pull-right desktop"> <div class="col-3 pull-right desktop">
<div class="row list-controls"> <div class="row list-controls">
<button @click="$emit('consume')" :class="{enabled: status.consume}" title="Toggle consume mode" v-if="buttons.consume"> <button @click="$emit('consume')" :class="{enabled: status.consume}" title="Toggle consume mode" v-if="buttons_.consume">
<i class="icon fa fa-utensils"></i> <i class="icon fa fa-utensils"></i>
</button> </button>
<button @click="$emit('random')" :class="{enabled: status.random}" title="Toggle shuffle" v-if="buttons.random"> <button @click="$emit('random')" :class="{enabled: status.random}" title="Toggle shuffle" v-if="buttons_.random">
<i class="icon fa fa-random"></i> <i class="icon fa fa-random"></i>
</button> </button>
<button @click="$emit('repeat')" :class="{enabled: status.repeat}" title="Toggle repeat" v-if="buttons.repeat"> <button @click="$emit('repeat')" :class="{enabled: status.repeat}" title="Toggle repeat" v-if="buttons_.repeat">
<i class="icon fa fa-redo"></i> <i class="icon fa fa-redo"></i>
</button> </button>
</div> </div>
@ -202,10 +203,20 @@ export default {
}, },
data() { data() {
const buttons = Object.keys(this.buttons)?.length ? this.buttons : {
previous: true,
next: true,
stop: true,
consume: true,
random: true,
repeat: true,
}
return { return {
expanded: false, expanded: false,
lastSync: 0, lastSync: 0,
elapsed: this.status?.elapsed, elapsed: this.status?.elapsed || this.status?.position,
buttons_: buttons,
} }
}, },
@ -235,8 +246,11 @@ export default {
}) })
setInterval(() => { setInterval(() => {
if (self.status?.state !== 'stop') {
self.elapsed = (self.status?.elapsed || self.status?.position || 0)
if (self.status?.state === 'play') if (self.status?.state === 'play')
self.elapsed = (self.status?.elapsed || 0) + Math.round(this.getTime() - self.lastSync) self.elapsed += Math.round(this.getTime() - self.lastSync)
}
}, 1000) }, 1000)
}, },
} }

View file

@ -4,10 +4,11 @@
<slot /> <slot />
</div> </div>
<div class="controls-container"> <div class="controls-container">
<Controls :status="status" :track="track" @play="$emit('play', $event)" @pause="$emit('pause', $event)" <Controls :status="status" :track="track" :buttons="buttons" @play="$emit('play', $event)"
@stop="$emit('stop')" @previous="$emit('previous')" @next="$emit('next')" @seek="$emit('seek', $event)" @pause="$emit('pause', $event)" @stop="$emit('stop')" @previous="$emit('previous')"
@set-volume="$emit('set-volume', $event)" @consume="$emit('consume', $event)" @next="$emit('next')" @seek="$emit('seek', $event)" @set-volume="$emit('set-volume', $event)"
@repeat="$emit('repeat', $event)" @random="$emit('random', $event)" @search="$emit('search', $event)" /> @consume="$emit('consume', $event)" @repeat="$emit('repeat', $event)" @random="$emit('random', $event)"
@search="$emit('search', $event)"/>
</div> </div>
</div> </div>
</template> </template>
@ -33,6 +34,10 @@ export default {
track: { track: {
type: Object, type: Object,
}, },
buttons: {
type: Object,
},
}, },
} }
</script> </script>

View file

@ -13,7 +13,7 @@
<i :class="icons[name].class" v-if="icons[name]?.class" /> <i :class="icons[name].class" v-if="icons[name]?.class" />
<i class="fas fa-puzzle-piece" v-else /> <i class="fas fa-puzzle-piece" v-else />
</span> </span>
<span class="name" v-if="!collapsed">{{ displayName(name) }}</span> <span class="name" v-if="!collapsed" v-text="name" />
</a> </a>
</li> </li>
</ul> </ul>
@ -44,10 +44,6 @@ export default {
}, },
methods: { methods: {
displayName(name) {
return name.split('.').map((token) => token[0].toUpperCase() + token.slice(1)).join(' ')
},
onItemClick(name) { onItemClick(name) {
this.$emit('select', name) this.$emit('select', name)
this.collapsed = true this.collapsed = true
@ -124,10 +120,6 @@ nav {
.icon { .icon {
margin-right: 0.5em; margin-right: 0.5em;
} }
.name {
text-transform: capitalize;
}
} }
.toggler { .toggler {

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="dropdown-container" ref="container"> <div class="dropdown-container" ref="container">
<button :title="title" ref="button" @click.stop="toggle"> <button :title="title" ref="button" @click.stop="toggle($event)">
<i class="icon" :class="iconClass" v-if="iconClass" /> <i class="icon" :class="iconClass" v-if="iconClass" />
<span class="text" v-text="text" v-if="text" /> <span class="text" v-text="text" v-if="text" />
</button> </button>
@ -83,7 +83,9 @@ export default {
}, 10) }, 10)
}, },
toggle() { toggle(event) {
event.stopPropagation()
this.$emit('click')
this.visible ? this.close() : this.open() this.visible ? this.close() : this.open()
}, },
}, },

View file

@ -0,0 +1,153 @@
<template>
<div class="header" :class="{'with-filter': filterVisible}">
<div class="row">
<div class="col-7 left side">
<button title="Filter" @click="filterVisible = !filterVisible">
<i class="fa fa-filter" />
</button>
<form @submit.prevent="search">
<label class="search-box">
<input type="search" placeholder="Search" v-model="query">
</label>
</form>
</div>
<div class="col-5 right side">
<Players :plugin-name="pluginName" @select="$emit('select-player', $event)"
@status="$emit('player-status', $event)" />
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
<DropdownItem text="Play URL" icon-class="fa fa-play-circle" />
</Dropdown>
</div>
</div>
<div class="row filter fade-in" :class="{hidden: !filterVisible}">
<label v-for="source in Object.keys(sources)" :key="source">
<input type="checkbox" v-model="sources[source]" />
{{ source }}
</label>
</div>
</div>
</template>
<script>
import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
import Players from "@/components/panels/Media/Players";
export default {
name: "Header",
components: {Players, DropdownItem, Dropdown},
emits: ['search', 'select-player', 'player-status'],
props: {
pluginName: {
type: String,
required: true,
},
},
data() {
return {
filterVisible: false,
query: '',
sources: {
'file': true,
'youtube': true,
'torrent': true,
},
}
},
methods: {
search() {
const types = Object.keys(this.sources).filter((source) => this.sources[source])
if (!this.query?.length || !types?.length)
return
this.$emit('search', {
query: this.query,
types: types,
})
},
}
}
</script>
<style lang="scss" scoped>
$media-header-height: 3.3em;
$filter-header-height: 3em;
.header {
width: 100%;
height: $media-header-height;
position: relative;
background: $menu-panel-bg;
padding: .5em;
box-shadow: $border-shadow-bottom;
.row {
display: flex;
align-items: center;
}
&.with-filter {
height: calc(#{$media-header-height} + #{$filter-header-height});
}
.side {
display: inline-flex;
align-items: center;
&.right {
justify-content: right;
}
}
::v-deep(button) {
background: none;
padding: 0 .25em;
border: 0;
margin-right: .25em;
&:hover {
color: $default-hover-fg-2;
}
}
form {
width: 100%;
padding: 0;
border: 0;
border-radius: 0;
box-shadow: none;
background: initial;
}
.search-box {
width: 100%;
margin-left: .5em;
input[type=search] {
width: 100%;
}
}
.filter {
position: absolute;
top: $media-header-height;
height: $filter-header-height;
padding-bottom: 1em;
label {
display: inline-flex;
flex-direction: row;
margin-right: 1em;
input {
margin-right: .5em;
}
}
}
}
</style>

View file

@ -0,0 +1,151 @@
<template>
<keep-alive>
<div class="media-plugin fade-in">
<Loading v-if="loading" />
<MediaView :plugin-name="pluginName" :status="selectedPlayer?.status || {}" :track="selectedPlayer?.status || {}"
:buttons="mediaButtons" @play="pause" @pause="pause" @stop="stop" @set-volume="setVolume"
@seek="seek" @search="search">
<main>
<Header :plugin-name="pluginName" @search="search" @select-player="selectedPlayer = $event"
@player-status="onStatusUpdate" />
<Results :results="results" :selected-result="selectedResult" @select="selectedResult = $event"
@play="play" @info="$refs.mediaInfo.isVisible = true" />
</main>
</MediaView>
<div class="media-info-container">
<Modal title="Media info" ref="mediaInfo">
<Info :item="results[selectedResult]" v-if="selectedResult != null" />
</Modal>
</div>
</div>
</keep-alive>
</template>
<script>
import Loading from "@/components/Loading";
import Modal from "@/components/Modal";
import Utils from "@/Utils";
import MediaView from "@/components/Media/View";
import Header from "@/components/panels/Media/Header";
import Info from "@/components/panels/Media/Info";
import Results from "@/components/panels/Media/Results";
export default {
name: "Media",
mixins: [Utils],
components: {Loading, MediaView, Header, Results, Modal, Info},
props: {
pluginName: {
type: String,
required: true,
},
mediaButtons: {
type: Object,
default: () => {
return {
previous: false,
next: false,
stop: true,
}
}
}
},
data() {
return {
loading: false,
results: [],
selectedResult: null,
selectedPlayer: null,
}
},
methods: {
async search(event) {
this.loading = true
try {
this.results = await this.request(`${this.pluginName}.search`, event)
} finally {
this.loading = false
}
},
async play(item) {
await this.selectedPlayer.component.play(item, this.selectedPlayer)
await this.refresh()
},
async pause() {
await this.selectedPlayer.component.pause(this.selectedPlayer)
await this.refresh()
},
async stop() {
await this.selectedPlayer.component.stop(this.selectedPlayer)
await this.refresh()
},
async setVolume(volume) {
await this.selectedPlayer.component.setVolume(volume, this.selectedPlayer)
await this.refresh()
},
async seek(position) {
await this.selectedPlayer.component.seek(position, this.selectedPlayer)
await this.refresh()
},
async refresh() {
this.selectedPlayer.status = await this.selectedPlayer.component.status(this.selectedPlayer)
},
onStatusUpdate(status) {
if (!this.selectedPlayer)
return
this.selectedPlayer.status = status
}
},
mounted() {
this.$watch(() => this.selectedPlayer, (player) => {
if (player)
this.refresh()
})
},
}
</script>
<style lang="scss" scoped>
.media-plugin {
width: 100%;
main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
}
::v-deep(.loading) {
z-index: 10;
}
::v-deep(.media-info-container) {
.modal-container {
.content {
max-width: 75%;
}
.body {
padding: 1em .5em;
overflow: auto;
}
}
}
</style>

View file

@ -0,0 +1,159 @@
<template>
<div class="row" v-if="item?.title">
<div class="left side">Title</div>
<div class="right side">
<a :href="`https://www.imdb.com/title/${item.imdb_id}`" target="_blank" v-if="item.imdb_id"
v-text="item.title" />
<span v-else v-text="item.title" />
</div>
</div>
<div class="row" v-if="item?.synopsis">
<div class="left side">Synopsis</div>
<div class="right side" v-text="item.synopsis" />
</div>
<div class="row" v-if="item?.description">
<div class="left side">Description</div>
<div class="right side" v-text="item.description" />
</div>
<div class="row" v-if="item?.channelId">
<div class="left side">Channel</div>
<div class="right side">
<a :href="`https://www.youtube.com/channel/${item.channelId}`" target="_blank"
v-text="item.channelTitle || `https://www.youtube.com/channel/${item.channelId}`" />
</div>
</div>
<div class="row" v-if="item?.year">
<div class="left side">Year</div>
<div class="right side" v-text="item.year" />
</div>
<div class="row" v-if="item?.publishedAt">
<div class="left side">Published at</div>
<div class="right side" v-text="formatDate(item.publishedAt, true)" />
</div>
<div class="row" v-if="item?.file">
<div class="left side">File</div>
<div class="right side" v-text="item.file" />
</div>
<div class="row" v-if="item?.url">
<div class="left side">URL</div>
<div class="right side url">
<a :href="item.url" target="_blank" v-text="item.url" />
</div>
</div>
<div class="row" v-if="item?.trailer">
<div class="left side">Trailer</div>
<div class="right side url">
<a :href="item.trailer" target="_blank" v-text="item.trailer" />
</div>
</div>
<div class="row" v-if="item?.size">
<div class="left side">Size</div>
<div class="right side" v-text="convertSize(item.size)" />
</div>
<div class="row" v-if="item?.quality">
<div class="left side">Quality</div>
<div class="right side" v-text="item.quality" />
</div>
<div class="row" v-if="item?.seeds">
<div class="left side">Seeds</div>
<div class="right side" v-text="item.seeds" />
</div>
<div class="row" v-if="item?.peers">
<div class="left side">Peers</div>
<div class="right side" v-text="item.peers" />
</div>
<div class="row" v-if="item?.language">
<div class="left side">Language</div>
<div class="right side" v-text="item.language" />
</div>
</template>
<script>
import Utils from "@/Utils";
export default {
name: "Info",
mixins: [Utils],
props: {
item: {
type: Object,
default: () => {},
}
}
}
</script>
<style lang="scss" scoped>
.row {
display: flex;
min-height: 3em;
padding: .5em 1em;
@include until ($tablet) {
flex-direction: column;
}
@include from ($tablet) {
align-items: center;
}
&:not(:last-child) {
border-bottom: $default-border-2;
}
&:hover {
background: $hover-bg;
border-radius: .5em;
}
.side {
align-items: center;
display: inline-flex;
&.url {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@include until ($tablet) {
display: flex;
&.left {
font-weight: bold;
}
&.right {
justify-content: left;
}
}
@include from ($tablet) {
display: inline-flex;
&.left {
width: 22%;
margin-right: 3%;
}
&.right {
width: 75%;
justify-content: right;
}
}
}
}
</style>

View file

@ -0,0 +1,141 @@
<template>
<div class="plugins">
<Chromecast :player="selectedPlayer?.pluginName === 'media.chromecast' ? selectedPlayer : null"
ref="chromecastPlugin" @status="$emit('status', $event)" />
<Kodi :player="selectedPlayer?.pluginName === 'media.kodi' ? selectedPlayer : null" ref="kodiPlugin"
@status="$emit('status', $event)" />
<Mplayer :player="selectedPlayer?.pluginName === 'media.mplayer' ? selectedPlayer : null" ref="mplayerPlugin"
@status="$emit('status', $event)" />
<Mpv :player="selectedPlayer?.pluginName === 'media.mpv' ? selectedPlayer : null" ref="mpvPlugin"
@status="$emit('status', $event)" />
<Omxplayer :player="selectedPlayer?.pluginName === 'media.omxplayer' ? selectedPlayer : null" ref="omxplayerPlugin"
@status="$emit('status', $event)" />
<Vlc :player="selectedPlayer?.pluginName === 'media.vlc' ? selectedPlayer : null" ref="vlcPlugin"
@status="$emit('status', $event)" />
</div>
<div class="players">
<Dropdown :title="selectedPlayer?.name || 'Players'"
:icon-class="selectedPlayer ? selectedPlayer.iconClass : 'fab fa-chromecast'">
<Loading v-if="loading" />
<div class="refresh">
<DropdownItem text="Refresh" icon-class="fa fa-sync-alt" @click="refresh" />
</div>
<div class="no-results" v-if="!players?.length">No players found</div>
<div class="player" v-for="(player, i) in players" :key="i"
:class="{selected: selectedPlayer != null && selectedPlayer.pluginName === player.pluginName
&& selectedPlayer.name === player.name}">
<DropdownItem :text="player.name" :icon-class="player.iconClass" @click="select(player)" />
</div>
</Dropdown>
</div>
</template>
<script>
import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
import Loading from "@/components/Loading";
import Chromecast from "@/components/panels/Media/Players/Chromecast"
import Kodi from "@/components/panels/Media/Players/Kodi";
import Mplayer from "@/components/panels/Media/Players/Mplayer";
import Mpv from "@/components/panels/Media/Players/Mpv";
import Omxplayer from "@/components/panels/Media/Players/Omxplayer";
import Vlc from "@/components/panels/Media/Players/Vlc";
export default {
name: "Players",
components: {Loading, DropdownItem, Dropdown, Chromecast, Kodi, Mplayer, Mpv, Omxplayer, Vlc},
emits: ['select', 'status'],
props: {
pluginName: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
players: [],
selectedPlayer: null,
config: {},
plugins: [],
}
},
methods: {
loadPlugins() {
this.plugins = Object.entries(this.$refs).filter((p) => p[0].endsWith('Plugin')).map((p) => p[1])
},
async refresh() {
this.players = []
this.loading = true
const config = this.$root.config
try {
await Promise.all(this.plugins.map(async (plugin) => {
if (!(plugin.pluginName in config))
return
const players = await plugin.getPlayers()
this.players.push(...players)
if (this.selectedPlayer == null && plugin.pluginName === this.pluginName && players.length > 0) {
this.select(players[0])
}
}))
} finally {
this.loading = false
}
},
select(player) {
this.selectedPlayer = player
this.$emit('select', player)
},
},
async mounted() {
await this.loadPlugins()
await this.refresh()
}
}
</script>
<style lang="scss" scoped>
.plugins {
display: none;
}
.no-results {
padding: 1em;
}
.players {
::v-deep(.dropdown) {
.item {
padding: .5em;
}
.icon {
margin-right: 1em !important;
}
}
::v-deep(.refresh) {
font-weight: bold;
font-size: .8em;
opacity: .7;
}
::v-deep(.player.selected) {
color: $selected-fg;
}
}
</style>

View file

@ -0,0 +1,80 @@
<template>
<div />
</template>
<script>
import Mixin from "@/components/panels/Media/Players/Mixin";
export default {
name: "Chromecast",
mixins: [Mixin],
data() {
return {
name: 'Chromecast',
pluginName: 'media.chromecast',
iconClass: 'fab fa-chromecast',
}
},
methods: {
async getPlayers() {
const devices = await this.request(`${this.pluginName}.get_chromecasts`)
return Promise.all(devices.map(async (device) => {
return {
...device,
iconClass: device.type === 'audio' ? 'fa fa-volume-up' : 'fab fa-chromecast',
pluginName: this.pluginName,
status: this.request(`${this.pluginName}.status`, {chromecast: device.name}),
component: this,
}
}))
},
getPlayerName(player) {
if (typeof player === 'string')
return player
if (!player)
return this.player?.name
return player?.name
},
async status(player) {
return await this.request(`${this.pluginName}.status`, {chromecast: this.getPlayerName(player)})
},
async play(resource, player) {
if (!resource) {
return await this.pause(player)
}
return await this.request(`${this.pluginName}.play`, {resource: resource.url, chromecast: this.getPlayerName(player)})
},
async pause(player) {
return await this.request(`${this.pluginName}.pause`, {chromecast: this.getPlayerName(player)})
},
async stop(player) {
return await this.request(`${this.pluginName}.stop`, {chromecast: this.getPlayerName(player)})
},
async setVolume(volume, player) {
return await this.request(`${this.pluginName}.set_volume`, {volume: volume, chromecast: this.getPlayerName(player)})
},
async seek(position, player) {
return await this.request(`${this.pluginName}.seek`, {position: position, chromecast: this.getPlayerName(player)})
},
async onMediaEvent(event) {
if (event.plugin !== this.pluginName)
return false
this.$emit('status', await this.status(event.player))
return true
},
},
}
</script>

View file

@ -0,0 +1,31 @@
<template>
<div />
</template>
<script>
import Mixin from "@/components/panels/Media/Players/Mixin";
export default {
name: "Kodi",
mixins: [Mixin],
data() {
return {
iconClass: 'fa fa-kodi',
name: 'Kodi',
pluginName: 'media.kodi',
}
},
methods: {
async getPlayers() {
return [{
iconClass: this.iconClass,
pluginName: this.pluginName,
name: this.$root.config['media.kodi']?.host || this.name,
component: this,
status: await this.request(`${this.pluginName}.status`)
}]
}
},
}
</script>

View file

@ -0,0 +1,102 @@
<script>
import Utils from "@/Utils";
export default {
name: "Mixin",
mixins: [Utils],
emits: ['status'],
props: {
player: {
type: Object,
},
},
data() {
return {
iconClass: null,
name: null,
pluginName: null,
}
},
methods: {
async getPlayers() {
return [{
iconClass: this.iconClass,
name: this.name,
pluginName: this.pluginName,
component: this,
status: await this.status(),
}]
},
async status() {
return await this.request(`${this.pluginName}.status`)
},
async play(resource) {
if (!resource) {
return await this.pause()
}
return await this.request(`${this.pluginName}.play`, {resource: resource.url})
},
async pause() {
return await this.request(`${this.pluginName}.pause`)
},
async stop() {
return await this.request(`${this.pluginName}.stop`)
},
async setVolume(volume) {
return await this.request(`${this.pluginName}.set_volume`, {volume: volume})
},
async seek(position) {
return await this.request(`${this.pluginName}.seek`, {position: position})
},
async onNewMedia(event) {
const isMine = await this.onMediaEvent(event)
if (isMine && event.title) {
this.notify({
title: event.player || event.device || this.player?.name || this.name || this.pluginName,
text: event.title,
image: {
iconClass: this.iconClass || 'fa fa-play',
}
})
}
},
async onMediaEvent(event) {
if (event.plugin !== this.pluginName)
return false
this.$emit('status', await this.status())
return true
},
},
mounted() {
this.subscribe(this.onNewMedia, `on-new-media-${this.pluginName}`,
'platypush.message.event.media.NewPlayingMediaEvent')
this.subscribe(this.onMediaEvent, `on-media-event-${this.pluginName}`,
'platypush.message.event.media.MediaPlayEvent',
'platypush.message.event.media.MediaStopEvent',
'platypush.message.event.media.MediaPauseEvent',
'platypush.message.event.media.MediaSeekEvent',
'platypush.message.event.media.MediaVolumeChangedEvent',
'platypush.message.event.media.MediaMuteChangedEvent')
},
destroy() {
this.unsubscribe(`on-media-event-${this.pluginName}`)
},
}
</script>

View file

@ -0,0 +1,19 @@
<template>
<div />
</template>
<script>
import Mixin from "@/components/panels/Media/Players/Mixin";
export default {
name: "Mplayer",
mixins: [Mixin],
data() {
return {
iconClass: 'fa fa-tv',
name: 'MPlayer',
pluginName: 'media.mplayer',
}
},
}
</script>

View file

@ -0,0 +1,19 @@
<template>
<div />
</template>
<script>
import Mixin from "@/components/panels/Media/Players/Mixin";
export default {
name: "Mpv",
mixins: [Mixin],
data() {
return {
iconClass: 'fa fa-tv',
name: 'mpv',
pluginName: 'media.mpv',
}
},
}
</script>

View file

@ -0,0 +1,19 @@
<template>
<div />
</template>
<script>
import Mixin from "@/components/panels/Media/Players/Mixin";
export default {
name: "Omxplayer",
mixins: [Mixin],
data() {
return {
iconClass: 'fa fa-tv',
name: 'OMXPlayer',
pluginName: 'media.omxplayer',
}
},
}
</script>

View file

@ -0,0 +1,19 @@
<template>
<div />
</template>
<script>
import Mixin from "@/components/panels/Media/Players/Mixin";
export default {
name: "Vlc",
mixins: [Mixin],
data() {
return {
iconClass: 'fa fa-tv',
name: 'VLC',
pluginName: 'media.vlc',
}
},
}
</script>

View file

@ -0,0 +1,98 @@
<template>
<div class="media-results">
<div class="row item" :class="{selected: selectedResult === i}" v-for="(result, i) in results" :key="i"
@click="$emit('select', i)">
<div class="col-10 left side">
<div class="icon">
<i :class="typeIcons[result.type]" />
</div>
<div class="title" v-text="result.title" />
</div>
<div class="col-2 right side">
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h" @click="$emit('select', i)">
<DropdownItem icon-class="fa fa-play" text="Play" @click="$emit('play', result)" />
<DropdownItem icon-class="fas fa-closed-captioning" text="Play with captions" />
<DropdownItem icon-class="fa fa-info" text="Info" @click="$emit('info', result)" />
</Dropdown>
</div>
</div>
</div>
</template>
<script>
import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
export default {
name: "Results",
components: {Dropdown, DropdownItem},
emits: ['select', 'info', 'play'],
props: {
results: {
type: Array,
default: () => [],
},
selectedResult: {
type: Number,
},
},
data() {
return {
typeIcons: {
'file': 'fa fa-hdd',
'torrent': 'fa fa-magnet',
'youtube': 'fab fa-youtube',
},
}
},
}
</script>
<style lang="scss" scoped>
@import "items";
.media-results {
width: 100%;
height: 100%;
background: $background-color;
overflow: auto;
.item {
display: flex;
align-items: center;
&.selected {
background: $selected-bg;
}
.side {
display: inline-flex;
align-items: center;
&.right {
justify-content: right;
}
::v-deep(.dropdown-container) {
.item {
box-shadow: none;
}
button {
border: 0;
padding: 0;
background: none;
opacity: .7;
&:hover {
color: $default-hover-fg-2;
}
}
}
}
}
}
</style>

View file

@ -0,0 +1,60 @@
.item {
display: flex;
align-items: center;
padding: 1em .25em .5em .25em;
box-shadow: 0 2.5px 2px -1px $default-shadow-color;
cursor: pointer;
&:hover {
background: $hover-bg;
}
&.active {
background: $active-bg;
}
&.selected {
background: $selected-bg;
}
&.dragover {
border-top: 2px solid $default-hover-fg;
}
&::selection {
background: rgba(0, 0, 0, 0) !important;
}
.title {
font-size: 1em;
font-weight: normal;
margin: 0;
}
.right-side {
display: flex;
justify-content: right;
}
.duration,
.actions {
display: inline-flex;
align-items: center;
}
.duration {
font-size: .85em;
opacity: .7;
}
.actions {
::v-deep(button) {
opacity: .65;
}
}
.icon {
color: $default-fg-3;
margin-right: .75em;
}
}

View file

@ -0,0 +1,16 @@
<template>
<Media plugin-name="media.mplayer" />
</template>
<script>
import Media from '@/components/panels/Media/Index'
export default {
name: "MediaMplayer",
components: {Media},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,16 @@
<template>
<Media plugin-name="media.mpv" />
</template>
<script>
import Media from '@/components/panels/Media/Index'
export default {
name: "MediaMpv",
components: {Media},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,16 @@
<template>
<Media plugin-name="media.omxplayer" />
</template>
<script>
import Media from '@/components/panels/Media/Index'
export default {
name: "MediaMpv",
components: {Media},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,16 @@
<template>
<Media plugin-name="media.vlc" />
</template>
<script>
import Media from '@/components/panels/Media/Index'
export default {
name: "MediaVlc",
components: {Media},
}
</script>
<style scoped>
</style>

View file

@ -5,3 +5,4 @@
@import "components.scss"; @import "components.scss";
@import "inputs.scss"; @import "inputs.scss";
@import "animations.scss"; @import "animations.scss";
@import "icons.scss";

View file

@ -0,0 +1,8 @@
.fa.fa-kodi:before {
content: ' ';
background: url('/icons/kodi.svg');
background-size: 1em 1em;
width: 1em;
height: 1em;
display: inline-block;
}

View file

@ -6,7 +6,10 @@ $default-bg-4: #f1f3f2 !default;
$default-bg-5: #edf0ee !default; $default-bg-5: #edf0ee !default;
$default-bg-6: #e4eae8 !default; $default-bg-6: #e4eae8 !default;
$default-bg-7: #e4e4e4 !default; $default-bg-7: #e4e4e4 !default;
$default-fg: black !default; $default-fg: black !default;
$default-fg-2: #23513a !default;
$default-fg-3: #195331b3 !default;
//// Notifications //// Notifications
$notification-bg: rgba(185, 255, 193, 0.9) !default; $notification-bg: rgba(185, 255, 193, 0.9) !default;

View file

@ -2,11 +2,17 @@
export default { export default {
name: "DateTime", name: "DateTime",
methods: { methods: {
formatDate(date) { formatDate(date, year=false) {
return date.toDateString().substring(0, 10) if (typeof date === 'string')
date = new Date(Date.parse(date))
return date.toDateString().substring(0, year ? 14 : 10)
}, },
formatTime(date, seconds=true) { formatTime(date, seconds=true) {
if (typeof date === 'string')
date = new Date(Date.parse(date))
return date.toTimeString().substring(0, seconds ? 8 : 5) return date.toTimeString().substring(0, seconds ? 8 : 5)
}, },
}, },

View file

@ -15,6 +15,28 @@ export default {
return !!value return !!value
}, },
convertSize(value) {
if (typeof value === 'string')
value = parseInt(value)
let unit = null
const units = ['B', 'KB', 'MB', 'GB', 'TB']
units.forEach((u, i) => {
if (value <= 1024 && unit == null) {
unit = u
} else if (value > 1024) {
if (i === units.length-1) {
unit = u
} else {
value = value/1024
}
}
})
return `${value.toFixed(2)} ${unit}`
}
}, },
} }
</script> </script>

View file

@ -156,7 +156,6 @@ class MediaChromecastPlugin(MediaPlugin):
:type callback: func :type callback: func
""" """
import pychromecast
self.chromecasts.update({ self.chromecasts.update({
cast.device.friendly_name: cast cast.device.friendly_name: cast
for cast in self._get_chromecasts(tries=tries, retry_wait=retry_wait, for cast in self._get_chromecasts(tries=tries, retry_wait=retry_wait,