pifi 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23e41c0b8569a2e48e872dad40cae03b95e25c9b20b7b460ef3b957940a1ceeb
4
- data.tar.gz: 7df787b0e5c7f2774e7cf1905c61cdbebab5740b1cad98aaf56617d932178946
3
+ metadata.gz: 6abb671574e51a6e3e6f32104803cfda9cef3a660162afab4bfbc08b7610b65e
4
+ data.tar.gz: cfaee05fc1dfe51560216a511ae842642b15295f1f0afefe3aae09530ea6e28b
5
5
  SHA512:
6
- metadata.gz: a8466f2e04a688634b2ca3d9c9fb67b72e7325887b859998a20d192cd369b9967c8214cf516ffbb8935dd60f29a5ab935d8083245f33562462d4076d72c0ff85
7
- data.tar.gz: 6acae6f7cfed34a78358def99ecd5e30657ab6ee38baa4574b723d15aca00396e341fc739edc010b9b7874bd686ecfbec42a695d122793c30f6ab5a9a487cdad
6
+ metadata.gz: 54a24d455bf472d8417e54d38bf5d82896d78a7f3f92ce5baa22a9b3e2fefc5d5a9da4849cde8373765c18cacaa9acce24dac8204f5e95ec5ef71d9e52b37dc4
7
+ data.tar.gz: ea391430f652672666a0ccd82564e97ffe2e64d9f83ab24cf63a709a315cc7e30eb1d3a9d3d4cddcf188d3bded14a8be5e0113dd881259fb6b585ebb1e207158
data/INSTALL.md CHANGED
@@ -18,7 +18,7 @@ $ sudo systemctl start mpd && sudo systemctl enable mpd
18
18
  3. Install PiFi:
19
19
 
20
20
  ```
21
- $ sudo gem install pifi --no-ri --no-rdoc
21
+ $ sudo gem install pifi --no-document
22
22
  ```
23
23
 
24
24
  4. To run PiFi, you'll need a list of radios at `/etc/pifi/streams.json`. Paste this for now:
@@ -30,7 +30,7 @@ $ sudo wget https://raw.githubusercontent.com/rccavalcanti/pifi-radio/master/doc
30
30
 
31
31
  Later, you can edit that JSON file [as described here](README.md#list-of-streams).
32
32
 
33
- 5. If the MPD server is in other host or a non-default port, you'll need to place a configuration file.
33
+ 5. If the MPD server is on another host or a non-default port, you'll need to place a configuration file.
34
34
 
35
35
  Download a sample and edit [following the documentation](README.md#pifi-configuration):
36
36
 
@@ -61,6 +61,8 @@ $ sudo -e /etc/systemd/system/pifi.service
61
61
 
62
62
  **Done!** Now you can start PiFi with `sudo systemctl start pifi`. For running at boot, enter `sudo systemctl enable pifi`.
63
63
 
64
+ If this doesn't work, `journalctl -u pifi` should tell you what to fix.
65
+
64
66
  ## Advanced deployments
65
67
 
66
68
  If you need something different from this, [check this document](docs/install_tips.md).
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # PiFi Radio
2
2
 
3
- PiFi Radio: A MPD web client to listen to radio
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/rccavalcanti/pifi-radio/master/docs/icon/radio-original.svg?sanitize=true" width=250>
5
+ </p>
6
+ <p align="center">PiFi Radio: A MPD web client to listen to radio.</p>
4
7
 
5
8
  ## Table of contents
6
9
 
@@ -18,20 +21,18 @@ PiFi Radio: A MPD web client to listen to radio
18
21
 
19
22
  PiFi Radio is a MPD web client to listen to radio.
20
23
 
21
- If you have no idea what this means... You install it on a device such as the Raspberry Pi or even your computer. Actually anything with a speaker. Then, you open your browser from anywhere (e.g. your phone) and it will play your favorite radio stations.
24
+ If you have no idea what this means... You install it on a device such as the Raspberry Pi or anything with a speaker. Then, you open your browser from anywhere (e.g. your phone) to control it and listen to your favorite radio stations.
22
25
 
23
26
  PiFi is an interface for MPD, so it has some advantages compared to other solutions, e.g. bluetooth or AirPlay. One of them is that the playback is completely independent from your phone. So you can continue to use it normally, play a video, lose connection or even turn it off, and your Pi will still continue to play the radio.
24
27
 
25
28
  [I started this project in early 2017. At that time, I wanted to configure Raspbian so my parents could listen to radio with ease, but couldn't find any good solution to it.](https://rafaelc.org/blog/the-motivation-for-pifi-radio/)
26
29
 
27
-
28
30
  ## Some features
29
31
 
30
32
  - Responsive interface for phones, tablets and desktops.
31
33
  - Display stations clearly. No URLs or weird names.
34
+ - Themes, dark and light.
32
35
  - Easily search your radios.
33
- - Themes.
34
- - Straight-forward beautiful interface. Minimalist yet taking advantage of screen space for important information and controls.
35
36
  - Centralized list of stations. You get the same radios on every device.
36
37
  - Make some radios to be offered only to certain IPs. A use case for this is if there are tons of stations that only you listen and you don't want to pollute everyone else's list.
37
38
  - Organize your radios by categories.
@@ -41,7 +42,11 @@ PiFi is an interface for MPD, so it has some advantages compared to other soluti
41
42
 
42
43
  ## Demo
43
44
 
44
- Coming soon.
45
+ ![](docs/demo/mobile-01.png)
46
+
47
+ ![](docs/demo/desktop-01.png)
48
+
49
+ ![](docs/demo/desktop-02.png)
45
50
 
46
51
  ## Installation
47
52
 
data/docs/install_tips.md CHANGED
@@ -1,20 +1,20 @@
1
1
  # Advanced installation tips
2
2
 
3
- Here I list some tips on more advanced PiFi deployments.
3
+ Here I list some tips for more advanced PiFi deployments.
4
4
 
5
5
  ## Running on port 80
6
6
 
7
7
  You may want to access PiFi without typing a port, as in `http://pi.local` or `http://pifi.local`.
8
8
 
9
- For this, you can install a web server, such as Nginx, Apache or Lighttpd, and set up a reverse proxy. As the precise configuration varies between each of them, please check their documentation.
9
+ For this, you can install a web server, such as Nginx, Apache or Lighttpd, and set up a reverse proxy. As the precise configuration varies for each of them, please check their documentation.
10
10
 
11
11
  ## Serving static resources from your web server
12
12
 
13
- If you set your web server to reverse proxy PiFi, you may also use it to serve PiFi static assets. This possibly brings some performance gains.
13
+ If you set your web server to reverse proxy PiFi, you may also use it to serve PiFi static assets. This possibly brings performance gains.
14
14
 
15
15
  On PiFi configuration, set `serve_static` to false.
16
16
 
17
- On your web server, the exact steps depend on your configuration. You will probably need to set the root of the virtual server to PiFi's static assets directory. This is `${PIFI_DIR}/lib/pifi/public`. Refer to the next section for tips on how to find your `${PIFI_DIR}`. Refer to the documentation of your web server for details.
17
+ On your web server, the exact steps depend on your configuration. You will probably need to set the root of the virtual server to PiFi's static assets directory. This is `${PIFI_DIR}/lib/pifi/public`. Refer to the next section for tips on how to find your `${PIFI_DIR}` and to the documentation of your web server for details.
18
18
 
19
19
  ## Finding PiFi directory
20
20
 
@@ -24,6 +24,8 @@ This largely depends on how you installed PiFi and your distro.
24
24
  - If you installed with the `gem` command, run `gem environment` and check the paths below "Gem paths".
25
25
  - If you are completely lost, you can always run `find / -iname "pifi*" 2>/dev/null`
26
26
 
27
+ Note that the path contains both the Ruby version and the PiFi version. Thus, it can change whenever you update one of them. Be sure to edit your configuration accordingly.
28
+
27
29
  ## Configuring Thin directly
28
30
 
29
31
  The `pifi` command exposes most Rack CLI options, but you may want to access more advanced configurations from Thin (the application server PiFi uses).
@@ -35,7 +37,3 @@ For example, if you want to run PiFi on a socket instead of a port:
35
37
  ```
36
38
  $ thin start -R ${PIFI_DIR}/config.ru -e production --socket /var/run/pifi/pifi.sock
37
39
  ```
38
-
39
- ## Using other application server
40
-
41
- If for some reason you don't want to use Thin as the application server, simply run the one you want and pass a `production` flag and the path for PiFi's config.ru (`${PIFI_DIR}/config.ru`).
@@ -2,15 +2,6 @@ require "pifi/controllers/application_controller"
2
2
 
3
3
  module PiFi
4
4
  class IndexController < ApplicationController
5
- def title
6
- title = "PiFi Radio"
7
- settings.production? ? title : "[#{settings.environment.capitalize}] #{title}"
8
- end
9
-
10
- def play_local?
11
- settings.play_local
12
- end
13
-
14
5
  get "/" do
15
6
  send_file File.join(settings.public_folder, 'index.html')
16
7
  end
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "files": {
3
3
  "main.css": "/static/css/main.a50e54d2.chunk.css",
4
- "main.js": "/static/js/main.1ea4089a.chunk.js",
5
- "main.js.map": "/static/js/main.1ea4089a.chunk.js.map",
4
+ "main.js": "/static/js/main.f588df7b.chunk.js",
5
+ "main.js.map": "/static/js/main.f588df7b.chunk.js.map",
6
6
  "runtime-main.js": "/static/js/runtime-main.f04a0f25.js",
7
7
  "runtime-main.js.map": "/static/js/runtime-main.f04a0f25.js.map",
8
8
  "static/css/2.5dbdccff.chunk.css": "/static/css/2.5dbdccff.chunk.css",
9
9
  "static/js/2.2b057ab5.chunk.js": "/static/js/2.2b057ab5.chunk.js",
10
10
  "static/js/2.2b057ab5.chunk.js.map": "/static/js/2.2b057ab5.chunk.js.map",
11
11
  "index.html": "/index.html",
12
- "precache-manifest.b35a62c41b1e6f7d7178e760c633328f.js": "/precache-manifest.b35a62c41b1e6f7d7178e760c633328f.js",
12
+ "precache-manifest.5a9bc068aedf916dd7ceb2986c13099f.js": "/precache-manifest.5a9bc068aedf916dd7ceb2986c13099f.js",
13
13
  "service-worker.js": "/service-worker.js",
14
14
  "static/css/2.5dbdccff.chunk.css.map": "/static/css/2.5dbdccff.chunk.css.map",
15
15
  "static/css/main.a50e54d2.chunk.css.map": "/static/css/main.a50e54d2.chunk.css.map",
@@ -21,6 +21,6 @@
21
21
  "static/css/2.5dbdccff.chunk.css",
22
22
  "static/js/2.2b057ab5.chunk.js",
23
23
  "static/css/main.a50e54d2.chunk.css",
24
- "static/js/main.1ea4089a.chunk.js"
24
+ "static/js/main.f588df7b.chunk.js"
25
25
  ]
26
26
  }
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="description" content="MPD web client to listen to radio"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"><meta name="apple-mobile-web-app-title" content="PiFi Radio"><meta name="application-name" content="PiFi Radio"><meta name="msapplication-TileColor" content="#2b5797"><meta name="theme-color" content="#375a7f"><title>PiFi Radio</title><link href="" rel="stylesheet" title="theme"><link href="/static/css/2.5dbdccff.chunk.css" rel="stylesheet"><link href="/static/css/main.a50e54d2.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(l){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],f=0,i=[];f<n.length;f++)t=n[f],Object.prototype.hasOwnProperty.call(a,t)&&a[t]&&i.push(a[t][0]),a[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(l[r]=o[r]);for(s&&s(e);i.length;)i.shift()();return c.push.apply(c,u||[]),p()}function p(){for(var e,r=0;r<c.length;r++){for(var t=c[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==a[u]&&(n=!1)}n&&(c.splice(r--,1),e=f(f.s=t[0]))}return e}var t={},a={1:0},c=[];function f(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return l[e].call(r.exports,r,r.exports,f),r.l=!0,r.exports}f.m=l,f.c=t,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(r,e){if(1&e&&(r=f(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)f.d(t,n,function(e){return r[e]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var r=this.webpackJsonppifi_frontend=this.webpackJsonppifi_frontend||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;p()}([])</script><script src="/static/js/2.2b057ab5.chunk.js"></script><script src="/static/js/main.1ea4089a.chunk.js"></script></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="description" content="MPD web client to listen to radio"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"><meta name="apple-mobile-web-app-title" content="PiFi Radio"><meta name="application-name" content="PiFi Radio"><meta name="msapplication-TileColor" content="#2b5797"><meta name="theme-color" content="#375a7f"><title>PiFi Radio</title><link href="" rel="stylesheet" title="theme"><link href="/static/css/2.5dbdccff.chunk.css" rel="stylesheet"><link href="/static/css/main.a50e54d2.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(l){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],f=0,i=[];f<n.length;f++)t=n[f],Object.prototype.hasOwnProperty.call(a,t)&&a[t]&&i.push(a[t][0]),a[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(l[r]=o[r]);for(s&&s(e);i.length;)i.shift()();return c.push.apply(c,u||[]),p()}function p(){for(var e,r=0;r<c.length;r++){for(var t=c[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==a[u]&&(n=!1)}n&&(c.splice(r--,1),e=f(f.s=t[0]))}return e}var t={},a={1:0},c=[];function f(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return l[e].call(r.exports,r,r.exports,f),r.l=!0,r.exports}f.m=l,f.c=t,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(r,e){if(1&e&&(r=f(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)f.d(t,n,function(e){return r[e]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var r=this.webpackJsonppifi_frontend=this.webpackJsonppifi_frontend||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;p()}([])</script><script src="/static/js/2.2b057ab5.chunk.js"></script><script src="/static/js/main.f588df7b.chunk.js"></script></body></html>
@@ -1,6 +1,6 @@
1
1
  self.__precacheManifest = (self.__precacheManifest || []).concat([
2
2
  {
3
- "revision": "8ed46ca4fb6580dffd348ce8c33b8ad5",
3
+ "revision": "a569cb2c9ca92e5460caefeb7e9f426d",
4
4
  "url": "/index.html"
5
5
  },
6
6
  {
@@ -8,7 +8,7 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
8
8
  "url": "/static/css/2.5dbdccff.chunk.css"
9
9
  },
10
10
  {
11
- "revision": "e505a5852929f6b0a418",
11
+ "revision": "74ed1348d3544643a0cb",
12
12
  "url": "/static/css/main.a50e54d2.chunk.css"
13
13
  },
14
14
  {
@@ -20,8 +20,8 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
20
20
  "url": "/static/js/2.2b057ab5.chunk.js.LICENSE"
21
21
  },
22
22
  {
23
- "revision": "e505a5852929f6b0a418",
24
- "url": "/static/js/main.1ea4089a.chunk.js"
23
+ "revision": "74ed1348d3544643a0cb",
24
+ "url": "/static/js/main.f588df7b.chunk.js"
25
25
  },
26
26
  {
27
27
  "revision": "92793784c9eb98d269de",
@@ -14,7 +14,7 @@
14
14
  importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
15
15
 
16
16
  importScripts(
17
- "/precache-manifest.b35a62c41b1e6f7d7178e760c633328f.js"
17
+ "/precache-manifest.5a9bc068aedf916dd7ceb2986c13099f.js"
18
18
  );
19
19
 
20
20
  self.addEventListener('message', (event) => {
@@ -1,2 +1,2 @@
1
- (this.webpackJsonppifi_frontend=this.webpackJsonppifi_frontend||[]).push([[0],{18:function(e){e.exports=JSON.parse('{"b":["en","fr-FR","nl-NL","pt-BR"],"c":1000,"a":2500,"d":3000}')},42:function(e,t,a){e.exports=a.p+"static/media/logo.91554ce9.svg"},46:function(e,t,a){e.exports=a(95)},60:function(e,t,a){},81:function(e,t,a){},82:function(e,t,a){},83:function(e,t,a){},84:function(e,t,a){},85:function(e,t,a){},86:function(e,t,a){},87:function(e,t,a){},88:function(e,t,a){},90:function(e,t,a){},95:function(e,t,a){"use strict";a.r(t);var n=a(0),r=a.n(n),l=a(20),c=a.n(l),o=a(6),s=a.n(o),i=a(3),u=a(4),m=a(8),d=a(10),p=a(11),b=a(2),v=(a(60),a(42)),f=a.n(v),h=Object(b.c)()((function(e){var t=e.t;return r.a.createElement("nav",{className:"navbar navbar-expand-sm navbar-dark bg-primary "},r.a.createElement("a",{className:"navbar-brand",href:"/"},r.a.createElement("img",{src:f.a,width:"30",height:"30",className:"d-inline-block align-text-bottom mr-2",alt:""}),"PiFi Radio"),r.a.createElement("button",{className:"navbar-toggler",type:"button","data-toggle":"collapse","data-target":"#navbarNavAltMarkup","aria-controls":"navbarNavAltMarkup","aria-expanded":"false","aria-label":"Toggle navigation"},r.a.createElement("span",{className:"navbar-toggler-icon"})),r.a.createElement("div",{className:"collapse navbar-collapse",id:"navbarNavAltMarkup"},r.a.createElement("div",{className:"navbar-nav pt-1 ml-2"},r.a.createElement("button",{className:"btn btn-link nav-link","data-toggle":"modal","data-target":"#url-dialog"},t("playURL")),r.a.createElement("button",{className:"btn btn-link nav-link","data-toggle":"modal","data-target":"#settings"},t("settings")))))})),g=Object(b.c)()((function(e){var t=e.playerStatus,a=e.t,n=t.playing,l=t.title;return r.a.createElement("div",{className:"text-center"},r.a.createElement("h5",{className:"small text-uppercase"},a(n?"playing":"stopped")),r.a.createElement("h3",{className:"ellipsis"},l))})),y=a(15),E=a(14),k=a(27),N=a.n(k),w=a(5);N.a.interceptors.response.use(null,(function(e){var t=e.response&&e.response.status>=400&&e.response.status<500,a=!e.response;return t||a||w.b.error(r.a.createElement(b.a,null,(function(e,t){t.i18n;return r.a.createElement("p",null,e("errorUnexpected"))}))),e.response&&403===e.response.status&&w.b.error(r.a.createElement(b.a,null,(function(e,t){t.i18n;return r.a.createElement("p",null,e("errorForbidden"))}))),Promise.reject(e)}));var S={get:N.a.get,post:N.a.post},O="".concat("/api","/player");function j(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,a=new FormData;return a.set("method",e),t&&a.set("params",t),a}function C(){return S.post(O,j("play"))}function x(){return S.post(O,j("stop"))}function _(e){return S.post(O,j("change_vol",e))}function L(e){return S.post(O,j("play_radios",e))}function P(e){return S.post(O,j("play_urls",e))}var R=function(e){return e.playing?r.a.createElement("button",{className:"btn btn-danger",onClick:x},r.a.createElement(y.a,{icon:E.d})):r.a.createElement("button",{className:"btn btn-dark",onClick:C},r.a.createElement(y.a,{icon:E.c}))},T=a(18),A=(a(81),Object(b.c)()((function(e){var t=e.playerStatus,a=e.t,n=t.vol<0,l=function(e,t){return r.a.createElement("button",{className:"btn btn-dark p-3",disabled:n,onClick:function(){return function(e){var t,n,r,l;return s.a.async((function(c){for(;;)switch(c.prev=c.next){case 0:return c.next=2,s.a.awrap(_(e));case 2:t=c.sent,n=t.data,"vol",r="".concat(a("volume"),": ").concat(n,"%"),l={toastId:"vol",autoClose:T.d},w.b.isActive("vol")?w.b.update("vol",{render:r}):w.b.info(r,l);case 8:case"end":return c.stop()}}))}(e)}},r.a.createElement(y.a,{icon:t}))};return r.a.createElement("div",{className:"player-controls btn-group w-100"},l("-5",E.e),l("+5",E.f),r.a.createElement(R,{playing:t.playing}))}))),I=(a(82),function(){var e=document.querySelector("nav").clientHeight;document.querySelector(".player").style.top=e+24+"px"}),B=function(e){var t=e.playerStatus;return Object(n.useEffect)(I,[t]),r.a.createElement("div",{className:"player p-2"},r.a.createElement(g,{playerStatus:t}),r.a.createElement(A,{playerStatus:t}))},F=Object(b.c)()((function(e){var t=e.value,a=e.onChange,n=e.t;return r.a.createElement("input",{className:"form-control mb-4",type:"text",id:"query",placeholder:n("search"),autoComplete:"off",value:t,onChange:function(e){return a(e.target.value)},onFocus:function(){return a("")}})})),U=(a(83),function(){return r.a.createElement("div",{className:"loader"},r.a.createElement("div",{className:"spinner-border",role:"status"},r.a.createElement("span",{className:"sr-only"},"Loading...")))}),q="".concat("/api","/streams");a(84);var M=function(e){function t(){var e,a;Object(i.a)(this,t);for(var n=arguments.length,r=new Array(n),l=0;l<n;l++)r[l]=arguments[l];return(a=Object(m.a)(this,(e=Object(d.a)(t)).call.apply(e,[this].concat(r)))).state={streams:{},loading:!0,query:""},a.isPlaying=function(e){return e===a.props.playerStatus.title&&a.props.playerStatus.playing},a.handleItemClick=function(e){var t,n,r;return s.a.async((function(l){for(;;)switch(l.prev=l.next){case 0:if(t=a.props,n=t.t,r=t.onBackdrop,!a.isPlaying(e)){l.next=3;break}return l.abrupt("return");case 3:return r(n("tunning"),e),l.prev=4,l.next=7,s.a.awrap(L(e));case 7:l.next=12;break;case 9:l.prev=9,l.t0=l.catch(4),l.t0.response&&400===l.t0.response.status&&w.b.error(n("errorNotFound"));case 12:case"end":return l.stop()}}),null,null,[[4,9]])},a.getItemClasses=function(e){var t="streams__item ellipsis list-group-item ";return a.isPlaying(e)?t+"active":t+"list-group-item-action"},a.handleSearch=function(e){a.setState({query:e})},a}return Object(p.a)(t,e),Object(u.a)(t,[{key:"componentDidMount",value:function(){var e,t;return s.a.async((function(a){for(;;)switch(a.prev=a.next){case 0:return a.next=2,s.a.awrap(S.get(q));case 2:e=a.sent,t=e.data,this.setState({streams:t,loading:!1});case 5:case"end":return a.stop()}}),null,this)}},{key:"filteredStreams",value:function(){var e=this.state,t=e.streams,a=e.query;if(""===a)return t;var n={};for(var r in t)r.toLowerCase().includes(a.toLowerCase())&&!t[r]&&(n[r]=t[r]);return n}},{key:"renderList",value:function(){var e=this,t=this.filteredStreams();return 0===Object.keys(t).length?r.a.createElement("h4",{className:"p-4"},this.props.t("noStreams")):r.a.createElement("ul",{className:"list-group list-group-flush"},Object.keys(t).map((function(a){return t[a]?r.a.createElement("li",{className:"streams__header ellipsis",key:a},a):r.a.createElement("li",{className:e.getItemClasses(a),key:a,onClick:function(){return e.handleItemClick(a)}},a)})))}},{key:"render",value:function(){return this.state.loading?r.a.createElement(U,null):r.a.createElement("div",{className:"streams p-2"},r.a.createElement(F,{value:this.state.query,onChange:this.handleSearch}),this.renderList())}}]),t}(n.Component),D=Object(b.c)()(M),Y=(a(85),function(e){var t=e.playerStatus,a=t.title,n=t.playing;return r.a.createElement("div",{className:"mini-player p-3"},r.a.createElement("div",{className:"mini-player__left ellipsis pr-3"},r.a.createElement(y.a,{icon:E.b,className:"drawer__toggler mr-3"}),r.a.createElement("span",null,a)),r.a.createElement(R,{playing:n}))}),W=(a(86),function(e){function t(){var e,a;Object(i.a)(this,t);for(var n=arguments.length,r=new Array(n),l=0;l<n;l++)r[l]=arguments[l];return(a=Object(m.a)(this,(e=Object(d.a)(t)).call.apply(e,[this].concat(r)))).state={open:!1},a.toggle=function(){a.setState({open:!a.state.open})},a.handleClick=function(e){var t=e.target;"BUTTON"===t.tagName||"path"===t.tagName||(console.log("tag: ",t.tagName),console.log("class: ",t.className),a.toggle())},a.handleTouchStart=function(e){a.setState({touchStartY:e.touches[0].clientY})},a.handleTouchMove=function(e){var t=a.state,n=t.open,r=t.touchStartY,l=e.changedTouches[0].clientY,c=l<r;l>r&&n?a.toggle():c&&!n&&a.toggle()},a}return Object(p.a)(t,e),Object(u.a)(t,[{key:"render",value:function(){var e=this.props.playerStatus,t="drawer fixed-bottom bg-secondary border-top border-secondary";return this.state.open?(t+=" drawer--open",document.body.classList.add("body--drawer")):document.body.classList.remove("body--drawer"),r.a.createElement("div",{className:t,onClick:this.handleClick,onTouchStart:this.handleTouchStart,onTouchMove:this.handleTouchMove},this.state.open?r.a.createElement(r.a.Fragment,null,r.a.createElement(y.a,{icon:E.a,className:"drawer__toggler fa-lg mb-3"}),r.a.createElement(B,{playerStatus:e})," "):r.a.createElement(Y,{playerStatus:e}))}}]),t}(n.Component)),J=function(e){var t=e.id,a=e.title,n=e.footer,l=e.children;return r.a.createElement("div",{className:"modal fade",id:t,tabIndex:"-1",role:"dialog","aria-labelledby":"staticBackdropLabel","aria-hidden":"true"},r.a.createElement("div",{className:"modal-dialog",role:"document"},r.a.createElement("div",{className:"modal-content"},r.a.createElement("div",{className:"modal-header"},r.a.createElement("h5",{className:"modal-title",id:"staticBackdropLabel"},a),r.a.createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":"Close"},r.a.createElement("span",{"aria-hidden":"true"},"\xd7"))),r.a.createElement("div",{className:"modal-body"},l),r.a.createElement("div",{className:"modal-footer"},n))))},K="darkly",V="theme",G=function(e){return"https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/".concat(e,"/bootstrap.min.css")},H=[{id:"darkly",name:"Darkly",themeColor:"#375a7f"},{id:"lux",name:"Lux",themeColor:"#1a1a1a"},{id:"litera",name:"Litera",themeColor:"#4582ec"}];function $(){var e=z(),t=G(e),a=H.find((function(t){return t.id===e})).themeColor;document.querySelector('link[title="theme"]').setAttribute("href",t),document.querySelector('meta[name="theme-color"]').setAttribute("content",a)}function z(){var e=localStorage.getItem(V);return""===e?K:0===H.filter((function(t){return t.id===e})).length?(localStorage.removeItem(V),K):e}var Q=a(22),X=a(43),Z=function(e){var t=e.id,a=e.label,n=e.row,l=e.data,c=Object(X.a)(e,["id","label","row","data"]);return r.a.createElement("div",{className:n?"form-group row":"form-group"},r.a.createElement("label",{htmlFor:t,className:"col-sm-2 col-form-label"},a),r.a.createElement("div",{className:"col-sm-10"},r.a.createElement("select",Object.assign({id:t,className:"form-control"},c),l.map((function(e){return r.a.createElement("option",{key:e.id||e,value:e.id||e},e.name||e)})))))},ee=Object(b.c)()((function(e){var t=e.t;return r.a.createElement(J,{id:"settings",title:t("settings"),footer:r.a.createElement("button",{className:"btn btn-secondary","data-dismiss":"modal"},t("close"))},r.a.createElement(Z,{id:"theme-select",label:t("theme"),data:H,row:!0,value:z(),onChange:function(e){var t;t=e.target.value,localStorage.setItem(V,t),$()}}),r.a.createElement(Z,{id:"language-select",label:t("language"),data:T.b,row:!0,value:Q.a.language,onChange:function(e){Q.a.changeLanguage(e.target.value)}}))})),te=a(19),ae=Object(b.c)()((function(e){var t=e.t,a=Object(n.useState)(""),l=Object(te.a)(a,2),c=l[0],o=l[1],i=function(){""!==c&&(u(c),o(""))},u=function(e){var a,n;return s.a.async((function(r){for(;;)switch(r.prev=r.next){case 0:return a={type:w.b.TYPE.ERROR,render:t("errorNotFound")},n=Object(w.b)(t("tryingURL")),r.prev=2,r.next=5,s.a.awrap(P(e));case 5:r.next=10;break;case 7:r.prev=7,r.t0=r.catch(2),r.t0.response&&400===r.t0.response.status&&w.b.update(n,a);case 10:case"end":return r.stop()}}),null,null,[[2,7]])};return r.a.createElement(J,{id:"url-dialog",title:t("playURL"),footer:r.a.createElement(r.a.Fragment,null,r.a.createElement("button",{className:"btn btn-primary","data-dismiss":"modal",onClick:i},"OK"),r.a.createElement("button",{className:"btn btn-secondary",onClick:function(){return o("")},"data-dismiss":"modal"},"Cancel"))},r.a.createElement("input",{className:"form-control mb-4",type:"text",placeholder:"URL",value:c,onChange:function(e){var t=e.target;o(t.value)},onKeyDown:function(e){"Enter"===e.key&&i()}}))})),ne=(a(87),function(e){var t=e.title,a=e.body,l=Object(n.useState)(!1),c=Object(te.a)(l,2),o=c[0],s=c[1];Object(n.useEffect)((function(){t?(document.body.style.paddingRight=window.innerWidth-document.documentElement.clientWidth+"px",document.body.classList.add("body--backdrop"),s(!0)):!t&&o&&(document.body.style.paddingRight=0,document.body.classList.remove("body--backdrop"),s(!1))}),[t,a]);var i="backdrop p-2 text-white"+(t?" backdrop--visible":"");return r.a.createElement("div",{className:i},r.a.createElement("h3",{className:"text-white"},t),r.a.createElement("h5",{className:"text-white"},a))}),re=(a(88),a(89),function(e){function t(){var e,a;Object(i.a)(this,t);for(var n=arguments.length,r=new Array(n),l=0;l<n;l++)r[l]=arguments[l];return(a=Object(m.a)(this,(e=Object(d.a)(t)).call.apply(e,[this].concat(r)))).state={playerStatus:{},loading:!0,networkError:!1,backdrop:{}},a.handleBackdrop=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";a.setState({backdrop:{title:e,body:t}}),setTimeout((function(){return a.setState({backdrop:{}})}),T.a)},a}return Object(p.a)(t,e),Object(u.a)(t,[{key:"componentDidMount",value:function(){this.updatePlayerStatus()}},{key:"updatePlayerStatus",value:function(){var e,t,a=this;return s.a.async((function(n){for(;;)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,s.a.awrap(S.get(O));case 3:e=n.sent,t=e.data,this.setState({playerStatus:t,loading:!1,networkError:!1}),n.next=11;break;case 8:n.prev=8,n.t0=n.catch(0),this.setState({loading:!1,networkError:!0});case 11:setTimeout((function(){return a.updatePlayerStatus()}),T.c);case 12:case"end":return n.stop()}}),null,this,[[0,8]])}},{key:"render",value:function(){var e=this.state,t=e.loading,a=e.networkError,n=e.backdrop,l=e.playerStatus,c=this.props.t;return a?r.a.createElement(ne,{title:c("errorNetwork")}):t?r.a.createElement(U,null):l.con_mpd?r.a.createElement("div",{className:"app"},r.a.createElement(w.a,null),r.a.createElement(ne,{title:n.title,body:n.body}),r.a.createElement(h,null),r.a.createElement(W,{playerStatus:l}),r.a.createElement("main",{className:"app-main p-4"},r.a.createElement(B,{playerStatus:l}),r.a.createElement(D,{onBackdrop:this.handleBackdrop,playerStatus:l}),r.a.createElement(ee,null),r.a.createElement(ae,null))):r.a.createElement(ne,{title:c("disconnectedMPD")})}}]),t}(n.Component)),le=Object(b.c)()(re);Boolean("localhost"===window.location.hostname||"[::1]"===window.location.hostname||window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/));var ce=a(44),oe=a(45);Q.a.use(ce.a).use(oe.a).use(b.b).init({fallbackLng:"en",debug:Object({NODE_ENV:"production",PUBLIC_URL:"",REACT_APP_API_URL:"/api"}).REACT_APP_I18N_DEBUG,interpolation:{escapeValue:!1}});Q.a,a(90),a(91),a(92);$(),c.a.render(r.a.createElement(n.Suspense,{fallback:r.a.createElement(U,null)},r.a.createElement(le,null)),document.getElementById("root")),"serviceWorker"in navigator&&navigator.serviceWorker.ready.then((function(e){e.unregister()}))}},[[46,1,2]]]);
2
- //# sourceMappingURL=main.1ea4089a.chunk.js.map
1
+ (this.webpackJsonppifi_frontend=this.webpackJsonppifi_frontend||[]).push([[0],{18:function(e){e.exports=JSON.parse('{"b":["en","fr-FR","nl-NL","pt-BR"],"c":1000,"a":2500,"d":3000}')},42:function(e,t,a){e.exports=a.p+"static/media/logo.91554ce9.svg"},46:function(e,t,a){e.exports=a(95)},60:function(e,t,a){},81:function(e,t,a){},82:function(e,t,a){},83:function(e,t,a){},84:function(e,t,a){},85:function(e,t,a){},86:function(e,t,a){},87:function(e,t,a){},88:function(e,t,a){},90:function(e,t,a){},95:function(e,t,a){"use strict";a.r(t);var n=a(0),r=a.n(n),l=a(20),c=a.n(l),o=a(6),s=a.n(o),i=a(3),u=a(4),m=a(8),d=a(10),p=a(11),b=a(2),f=(a(60),a(42)),v=a.n(f),h=Object(b.c)()((function(e){var t=e.t;return r.a.createElement("nav",{className:"navbar navbar-expand-sm navbar-dark bg-primary "},r.a.createElement("a",{className:"navbar-brand",href:"/"},r.a.createElement("img",{src:v.a,width:"30",height:"30",className:"d-inline-block align-text-bottom mr-2",alt:""}),"PiFi Radio"),r.a.createElement("button",{className:"navbar-toggler",type:"button","data-toggle":"collapse","data-target":"#navbarNavAltMarkup","aria-controls":"navbarNavAltMarkup","aria-expanded":"false","aria-label":"Toggle navigation"},r.a.createElement("span",{className:"navbar-toggler-icon"})),r.a.createElement("div",{className:"collapse navbar-collapse",id:"navbarNavAltMarkup"},r.a.createElement("div",{className:"navbar-nav pt-1 ml-2"},r.a.createElement("button",{className:"btn btn-link nav-link","data-toggle":"modal","data-target":"#url-dialog"},t("playURL")),r.a.createElement("button",{className:"btn btn-link nav-link","data-toggle":"modal","data-target":"#settings"},t("settings")))))})),g=Object(b.c)()((function(e){var t=e.playerStatus,a=e.t,n=t.playing,l=t.title;return r.a.createElement("div",{className:"text-center"},r.a.createElement("h5",{className:"small text-uppercase"},a(n?"playing":"stopped")),r.a.createElement("h3",{className:"ellipsis"},l))})),y=a(15),E=a(14),k=a(27),N=a.n(k),w=a(5);N.a.interceptors.response.use(null,(function(e){var t=e.response&&e.response.status>=400&&e.response.status<500,a=!e.response;return t||a||w.b.error(r.a.createElement(b.a,null,(function(e,t){t.i18n;return r.a.createElement("p",null,e("errorUnexpected"))}))),e.response&&403===e.response.status&&w.b.error(r.a.createElement(b.a,null,(function(e,t){t.i18n;return r.a.createElement("p",null,e("errorForbidden"))}))),Promise.reject(e)}));var S={get:N.a.get,post:N.a.post},O="".concat("/api","/player");function C(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,a=new FormData;return a.set("method",e),t&&a.set("params",t),a}function j(){return S.post(O,C("play"))}function x(){return S.post(O,C("stop"))}function _(e){return S.post(O,C("change_vol",e))}function L(e){return S.post(O,C("play_radios",e))}function R(e){return S.post(O,C("play_urls",e))}var P=function(e){return e.playing?r.a.createElement("button",{className:"btn btn-danger",onClick:x},r.a.createElement(y.a,{icon:E.d})):r.a.createElement("button",{className:"btn btn-dark",onClick:j},r.a.createElement(y.a,{icon:E.c}))},T=a(18),A=(a(81),Object(b.c)()((function(e){var t=e.playerStatus,a=e.t,n=t.vol<0,l=function(e,t){return r.a.createElement("button",{className:"btn btn-dark p-3",disabled:n,onClick:function(){return function(e){var t,n,r,l;return s.a.async((function(c){for(;;)switch(c.prev=c.next){case 0:return c.next=2,s.a.awrap(_(e));case 2:t=c.sent,n=t.data,"vol",r="".concat(a("volume"),": ").concat(n,"%"),l={toastId:"vol",autoClose:T.d},w.b.isActive("vol")?w.b.update("vol",{render:r}):w.b.info(r,l);case 8:case"end":return c.stop()}}))}(e)}},r.a.createElement(y.a,{icon:t}))};return r.a.createElement("div",{className:"player-controls btn-group w-100"},l("-5",E.e),l("+5",E.f),r.a.createElement(P,{playing:t.playing}))}))),I=(a(82),function(){var e=document.querySelector("nav").clientHeight;document.querySelector(".player").style.top=e+24+"px"}),B=function(e){var t=e.playerStatus;return Object(n.useEffect)(I,[t]),r.a.createElement("div",{className:"player p-2"},r.a.createElement(g,{playerStatus:t}),r.a.createElement(A,{playerStatus:t}))},F=Object(b.c)()((function(e){var t=e.value,a=e.onChange,n=e.t;return r.a.createElement("input",{className:"form-control mb-4",type:"text",id:"query",placeholder:n("search"),autoComplete:"off",value:t,onChange:function(e){return a(e.target.value)},onFocus:function(){return a("")}})})),U=(a(83),function(){return r.a.createElement("div",{className:"loader"},r.a.createElement("div",{className:"spinner-border",role:"status"},r.a.createElement("span",{className:"sr-only"},"Loading...")))}),q="".concat("/api","/streams");a(84);var M=function(e){function t(){var e,a;Object(i.a)(this,t);for(var n=arguments.length,r=new Array(n),l=0;l<n;l++)r[l]=arguments[l];return(a=Object(m.a)(this,(e=Object(d.a)(t)).call.apply(e,[this].concat(r)))).state={streams:{},loading:!0,query:""},a.isPlaying=function(e){return e===a.props.playerStatus.title&&a.props.playerStatus.playing},a.handleItemClick=function(e){var t,n,r;return s.a.async((function(l){for(;;)switch(l.prev=l.next){case 0:if(t=a.props,n=t.t,r=t.onBackdrop,!a.isPlaying(e)){l.next=3;break}return l.abrupt("return");case 3:return r(n("tunning"),e),l.prev=4,l.next=7,s.a.awrap(L(e));case 7:l.next=12;break;case 9:l.prev=9,l.t0=l.catch(4),l.t0.response&&400===l.t0.response.status&&w.b.error(n("errorNotFound"));case 12:case"end":return l.stop()}}),null,null,[[4,9]])},a.getItemClasses=function(e){var t="streams__item ellipsis list-group-item ";return a.isPlaying(e)?t+"active":t+"list-group-item-action"},a.handleSearch=function(e){a.setState({query:e})},a}return Object(p.a)(t,e),Object(u.a)(t,[{key:"componentDidMount",value:function(){var e,t;return s.a.async((function(a){for(;;)switch(a.prev=a.next){case 0:return a.next=2,s.a.awrap(S.get(q));case 2:e=a.sent,t=e.data,this.setState({streams:t,loading:!1});case 5:case"end":return a.stop()}}),null,this)}},{key:"filteredStreams",value:function(){var e=this.state,t=e.streams,a=e.query;if(""===a)return t;var n={};for(var r in t)r.toLowerCase().includes(a.toLowerCase())&&!t[r]&&(n[r]=t[r]);return n}},{key:"renderList",value:function(){var e=this,t=this.filteredStreams();return 0===Object.keys(t).length?r.a.createElement("h4",{className:"p-4"},this.props.t("noStreams")):r.a.createElement("ul",{className:"list-group list-group-flush"},Object.keys(t).map((function(a){return t[a]?r.a.createElement("li",{className:"streams__header ellipsis",key:a},a):r.a.createElement("li",{className:e.getItemClasses(a),key:a,onClick:function(){return e.handleItemClick(a)}},a)})))}},{key:"render",value:function(){return this.state.loading?r.a.createElement(U,null):r.a.createElement("div",{className:"streams p-2"},r.a.createElement(F,{value:this.state.query,onChange:this.handleSearch}),this.renderList())}}]),t}(n.Component),D=Object(b.c)()(M),Y=(a(85),function(e){var t=e.playerStatus,a=t.title,n=t.playing;return r.a.createElement("div",{className:"mini-player p-3"},r.a.createElement("div",{className:"mini-player__left ellipsis pr-3"},r.a.createElement(y.a,{icon:E.b,className:"drawer__toggler mr-3"}),r.a.createElement("span",null,a)),r.a.createElement(P,{playing:n}))}),W=(a(86),function(e){function t(){var e,a;Object(i.a)(this,t);for(var n=arguments.length,r=new Array(n),l=0;l<n;l++)r[l]=arguments[l];return(a=Object(m.a)(this,(e=Object(d.a)(t)).call.apply(e,[this].concat(r)))).state={open:!1},a.toggle=function(){a.setState({open:!a.state.open})},a.handleClick=function(e){var t=e.target;"BUTTON"===t.tagName||"path"===t.tagName||(console.log("tag: ",t.tagName),console.log("class: ",t.className),a.toggle())},a.handleTouchStart=function(e){a.setState({touchStartY:e.touches[0].clientY})},a.handleTouchMove=function(e){var t=a.state,n=t.open,r=t.touchStartY,l=e.changedTouches[0].clientY,c=l<r;l>r&&n?a.toggle():c&&!n&&a.toggle()},a}return Object(p.a)(t,e),Object(u.a)(t,[{key:"render",value:function(){var e=this.props.playerStatus,t="drawer fixed-bottom bg-secondary border-top border-secondary";return this.state.open?(t+=" drawer--open",document.body.classList.add("body--drawer")):document.body.classList.remove("body--drawer"),r.a.createElement("div",{className:t,onClick:this.handleClick,onTouchStart:this.handleTouchStart,onTouchMove:this.handleTouchMove},this.state.open?r.a.createElement(r.a.Fragment,null,r.a.createElement(y.a,{icon:E.a,className:"drawer__toggler fa-lg mb-3"}),r.a.createElement(B,{playerStatus:e})," "):r.a.createElement(Y,{playerStatus:e}))}}]),t}(n.Component)),J=function(e){var t=e.id,a=e.title,n=e.footer,l=e.children;return r.a.createElement("div",{className:"modal fade",id:t,tabIndex:"-1",role:"dialog","aria-labelledby":"staticBackdropLabel","aria-hidden":"true"},r.a.createElement("div",{className:"modal-dialog",role:"document"},r.a.createElement("div",{className:"modal-content"},r.a.createElement("div",{className:"modal-header"},r.a.createElement("h5",{className:"modal-title",id:"staticBackdropLabel"},a),r.a.createElement("button",{type:"button",className:"close","data-dismiss":"modal","aria-label":"Close"},r.a.createElement("span",{"aria-hidden":"true"},"\xd7"))),r.a.createElement("div",{className:"modal-body"},l),r.a.createElement("div",{className:"modal-footer"},n))))},K="darkly",V="theme",G=function(e){return"https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/".concat(e,"/bootstrap.min.css")},H=[{id:"darkly",name:"Darkly",themeColor:"#375a7f"},{id:"lux",name:"Lux",themeColor:"#1a1a1a"},{id:"litera",name:"Litera",themeColor:"#4582ec"}];function $(){var e=z(),t=G(e),a=H.find((function(t){return t.id===e})).themeColor;document.querySelector('link[title="theme"]').setAttribute("href",t),document.querySelector('meta[name="theme-color"]').setAttribute("content",a)}function z(){var e=localStorage.getItem(V);return""===e?K:0===H.filter((function(t){return t.id===e})).length?(localStorage.removeItem(V),K):e}var Q=a(22),X=a(43),Z=function(e){var t=e.id,a=e.label,n=e.row,l=e.data,c=Object(X.a)(e,["id","label","row","data"]);return r.a.createElement("div",{className:n?"form-group row":"form-group"},r.a.createElement("label",{htmlFor:t,className:"col-sm-2 col-form-label"},a),r.a.createElement("div",{className:"col-sm-10"},r.a.createElement("select",Object.assign({id:t,className:"form-control"},c),l.map((function(e){return r.a.createElement("option",{key:e.id||e,value:e.id||e},e.name||e)})))))},ee=Object(b.c)()((function(e){var t=e.t;return r.a.createElement(J,{id:"settings",title:t("settings"),footer:r.a.createElement("button",{className:"btn btn-secondary","data-dismiss":"modal"},t("close"))},r.a.createElement(Z,{id:"theme-select",label:t("theme"),data:H,row:!0,value:z(),onChange:function(e){var t;t=e.target.value,localStorage.setItem(V,t),$()}}),r.a.createElement(Z,{id:"language-select",label:t("language"),data:T.b,row:!0,value:Q.a.language,onChange:function(e){Q.a.changeLanguage(e.target.value)}}),r.a.createElement("hr",{className:"mt-5"}),r.a.createElement("p",{className:"small"},"Copyright \xa9 2017-2019\xa0",r.a.createElement("a",{href:"https://rafaelc.org/",target:"_blank",rel:"noopener noreferrer"},"Rafael Cavalcanti")))})),te=a(19),ae=Object(b.c)()((function(e){var t=e.t,a=Object(n.useState)(""),l=Object(te.a)(a,2),c=l[0],o=l[1],i=function(){""!==c&&(u(c),o(""))},u=function(e){var a,n;return s.a.async((function(r){for(;;)switch(r.prev=r.next){case 0:return a={type:w.b.TYPE.ERROR,render:t("errorNotFound")},n=Object(w.b)(t("tryingURL")),r.prev=2,r.next=5,s.a.awrap(R(e));case 5:r.next=10;break;case 7:r.prev=7,r.t0=r.catch(2),r.t0.response&&400===r.t0.response.status&&w.b.update(n,a);case 10:case"end":return r.stop()}}),null,null,[[2,7]])};return r.a.createElement(J,{id:"url-dialog",title:t("playURL"),footer:r.a.createElement(r.a.Fragment,null,r.a.createElement("button",{className:"btn btn-primary","data-dismiss":"modal",onClick:i},"OK"),r.a.createElement("button",{className:"btn btn-secondary",onClick:function(){return o("")},"data-dismiss":"modal"},"Cancel"))},r.a.createElement("input",{className:"form-control mb-4",type:"text",placeholder:"URL",value:c,onChange:function(e){var t=e.target;o(t.value)},onKeyDown:function(e){"Enter"===e.key&&i()}}))})),ne=(a(87),function(e){var t=e.title,a=e.body,l=Object(n.useState)(!1),c=Object(te.a)(l,2),o=c[0],s=c[1];Object(n.useEffect)((function(){t?(document.body.style.paddingRight=window.innerWidth-document.documentElement.clientWidth+"px",document.body.classList.add("body--backdrop"),s(!0)):!t&&o&&(document.body.style.paddingRight=0,document.body.classList.remove("body--backdrop"),s(!1))}),[t,a]);var i="backdrop p-2 text-white"+(t?" backdrop--visible":"");return r.a.createElement("div",{className:i},r.a.createElement("h3",{className:"text-white"},t),r.a.createElement("h5",{className:"text-white"},a))}),re=(a(88),a(89),function(e){function t(){var e,a;Object(i.a)(this,t);for(var n=arguments.length,r=new Array(n),l=0;l<n;l++)r[l]=arguments[l];return(a=Object(m.a)(this,(e=Object(d.a)(t)).call.apply(e,[this].concat(r)))).state={playerStatus:{},loading:!0,networkError:!1,backdrop:{}},a.handleBackdrop=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";a.setState({backdrop:{title:e,body:t}}),setTimeout((function(){return a.setState({backdrop:{}})}),T.a)},a}return Object(p.a)(t,e),Object(u.a)(t,[{key:"componentDidMount",value:function(){this.updatePlayerStatus()}},{key:"updatePlayerStatus",value:function(){var e,t,a=this;return s.a.async((function(n){for(;;)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,s.a.awrap(S.get(O));case 3:e=n.sent,t=e.data,this.setState({playerStatus:t,loading:!1,networkError:!1}),n.next=11;break;case 8:n.prev=8,n.t0=n.catch(0),this.setState({loading:!1,networkError:!0});case 11:setTimeout((function(){return a.updatePlayerStatus()}),T.c);case 12:case"end":return n.stop()}}),null,this,[[0,8]])}},{key:"render",value:function(){var e=this.state,t=e.loading,a=e.networkError,n=e.backdrop,l=e.playerStatus,c=this.props.t;return a?r.a.createElement(ne,{title:c("errorNetwork")}):t?r.a.createElement(U,null):l.con_mpd?r.a.createElement("div",{className:"app"},r.a.createElement(w.a,null),r.a.createElement(ne,{title:n.title,body:n.body}),r.a.createElement(h,null),r.a.createElement(W,{playerStatus:l}),r.a.createElement("main",{className:"app-main p-4"},r.a.createElement(B,{playerStatus:l}),r.a.createElement(D,{onBackdrop:this.handleBackdrop,playerStatus:l}),r.a.createElement(ee,null),r.a.createElement(ae,null))):r.a.createElement(ne,{title:c("disconnectedMPD")})}}]),t}(n.Component)),le=Object(b.c)()(re);Boolean("localhost"===window.location.hostname||"[::1]"===window.location.hostname||window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/));var ce=a(44),oe=a(45);Q.a.use(ce.a).use(oe.a).use(b.b).init({fallbackLng:"en",debug:Object({NODE_ENV:"production",PUBLIC_URL:"",REACT_APP_API_URL:"/api"}).REACT_APP_I18N_DEBUG,interpolation:{escapeValue:!1}});Q.a,a(90),a(91),a(92);$(),c.a.render(r.a.createElement(n.Suspense,{fallback:r.a.createElement(U,null)},r.a.createElement(le,null)),document.getElementById("root")),"serviceWorker"in navigator&&navigator.serviceWorker.ready.then((function(e){e.unregister()}))}},[[46,1,2]]]);
2
+ //# sourceMappingURL=main.f588df7b.chunk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["logo.svg","components/navBar.jsx","components/playerStatus.jsx","services/httpService.js","services/playerService.js","components/playStopControl.jsx","components/playerControls.jsx","components/player.jsx","components/common/searchBox.jsx","components/loader.jsx","services/streamsService.js","components/streams.jsx","components/miniPlayer.jsx","components/drawer.jsx","components/common/modal.jsx","theme.js","components/common/select.jsx","components/settings.jsx","components/urlDialog.jsx","components/backdrop.jsx","App.js","serviceWorker.js","i18n.js","index.js"],"names":["module","exports","withTranslation","t","className","href","src","logo","width","height","alt","type","data-toggle","data-target","aria-controls","aria-expanded","aria-label","id","playerStatus","playing","title","axios","interceptors","response","use","error","expectedError","status","networkError","toast","i18n","Promise","reject","get","post","apiEndpoint","process","body","method","params","FormData","set","play","http","stop","changeVol","delta","playRadio","name","playURL","url","PlayStopControl","onClick","icon","faStop","faPlay","volDisabled","vol","renderVolButton","disabled","a","async","data","toastMsg","toastOpts","toastId","autoClose","volTimeout","isActive","update","render","info","handleVolChange","faVolumeDown","faVolumeUp","positionPlayerTop","navHeight","document","querySelector","clientHeight","style","top","Player","useEffect","value","onChange","placeholder","autoComplete","e","target","onFocus","Loader","role","Streams","state","streams","loading","query","isPlaying","props","handleItemClick","onBackdrop","getItemClasses","classes","handleSearch","setState","this","filtered","k","toLowerCase","includes","filteredStreams","Object","keys","length","map","key","renderList","Component","MiniPlayer","faChevronUp","Drawer","open","toggle","handleClick","tagName","console","log","handleTouchStart","touchStartY","touches","clientY","handleTouchMove","touchEndY","changedTouches","touchUp","classList","add","remove","onTouchStart","onTouchMove","Fragment","faChevronDown","Modal","footer","children","tabIndex","aria-labelledby","aria-hidden","data-dismiss","DEFAULT_THEME","STORAGE_KEY","getThemePath","themeId","themes","themeColor","applyTheme","getThemeId","themePath","find","setAttribute","localId","localStorage","getItem","filter","removeItem","Select","label","row","rest","htmlFor","d","setItem","languages","i18next","language","changeLanguage","rel","useState","setURL","handleOK","doPlayURL","errorToastOpts","TYPE","ERROR","input","onKeyDown","Backdrop","bodyStyled","setBodyStyled","paddingRight","window","innerWidth","documentElement","clientWidth","App","backdrop","handleBackdrop","setTimeout","backdropTimeout","updatePlayerStatus","updateInterval","con_mpd","Boolean","location","hostname","match","Backend","LanguageDetector","initReactI18next","init","fallbackLng","debug","REACT_APP_I18N_DEBUG","interpolation","escapeValue","ReactDOM","fallback","getElementById","navigator","serviceWorker","ready","then","registration","unregister"],"mappings":"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","file":"static/js/main.f588df7b.chunk.js","sourcesContent":["module.exports = __webpack_public_path__ + \"static/media/logo.91554ce9.svg\";","import React from 'react';\nimport { withTranslation } from 'react-i18next';\nimport './navBar.scss';\nimport logo from '../logo.svg';\n\nconst NavBar = ({ t }) => (\n <nav className=\"navbar navbar-expand-sm navbar-dark bg-primary \">\n <a className=\"navbar-brand\" href=\"/\">\n <img\n src={logo}\n width=\"30\"\n height=\"30\"\n className=\"d-inline-block align-text-bottom mr-2\"\n alt=\"\"\n />\n PiFi Radio\n </a>\n <button\n className=\"navbar-toggler\"\n type=\"button\"\n data-toggle=\"collapse\"\n data-target=\"#navbarNavAltMarkup\"\n aria-controls=\"navbarNavAltMarkup\"\n aria-expanded=\"false\"\n aria-label=\"Toggle navigation\"\n >\n <span className=\"navbar-toggler-icon\"></span>\n </button>\n <div className=\"collapse navbar-collapse\" id=\"navbarNavAltMarkup\">\n <div className=\"navbar-nav pt-1 ml-2\">\n {/* We use buttons and different class names instead of Bootstrap's\n anchor, so we don't get a warning for href=\"#\". */}\n <button\n className=\"btn btn-link nav-link\"\n data-toggle=\"modal\"\n data-target=\"#url-dialog\"\n >\n {t('playURL')}\n </button>\n\n <button\n className=\"btn btn-link nav-link\"\n data-toggle=\"modal\"\n data-target=\"#settings\"\n >\n {t('settings')}\n </button>\n </div>\n </div>\n </nav>\n);\nexport default withTranslation()(NavBar);\n","import React from 'react';\nimport { withTranslation } from 'react-i18next';\n\nconst PlayerStatus = ({ playerStatus, t }) => {\n const { playing, title } = playerStatus;\n\n return (\n <div className=\"text-center\">\n <h5 className=\"small text-uppercase\">\n {playing ? t('playing') : t('stopped')}\n </h5>\n <h3 className=\"ellipsis\">{title}</h3>\n </div>\n );\n};\n\nexport default withTranslation()(PlayerStatus);\n","import React from 'react';\nimport axios from 'axios';\nimport { toast } from 'react-toastify';\nimport { Translation } from 'react-i18next';\n\naxios.interceptors.response.use(null, error => {\n const expectedError =\n error.response &&\n error.response.status >= 400 &&\n error.response.status < 500;\n\n // We already alert about these with a backdrop on App.js\n const networkError = !error.response;\n\n // Ugly, but i18n only worked this way\n if (!expectedError && !networkError) {\n toast.error(\n <Translation>\n {(t, { i18n }) => <p>{t('errorUnexpected')}</p>}\n </Translation>\n );\n }\n // This expected error is universal in our app, so we'll place it here\n if (error.response && error.response.status === 403)\n toast.error(\n <Translation>{(t, { i18n }) => <p>{t('errorForbidden')}</p>}</Translation>\n );\n\n return Promise.reject(error);\n});\n\nexport default {\n get: axios.get,\n post: axios.post\n};\n","import http from './httpService.js';\n\nconst apiEndpoint = `${process.env.REACT_APP_API_URL}/player`;\n\nfunction body(method, params = null) {\n const body = new FormData();\n body.set('method', method);\n if (params) body.set('params', params);\n return body;\n}\n\nexport function getStatus() {\n return http.get(apiEndpoint);\n}\n\nexport function play() {\n return http.post(apiEndpoint, body('play'));\n}\n\nexport function stop() {\n return http.post(apiEndpoint, body('stop'));\n}\n\nexport function changeVol(delta) {\n return http.post(apiEndpoint, body('change_vol', delta));\n}\n\nexport function playRadio(name) {\n return http.post(apiEndpoint, body('play_radios', name));\n}\n\nexport function playURL(url) {\n return http.post(apiEndpoint, body('play_urls', url));\n}\n","import React from 'react';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faStop, faPlay } from '@fortawesome/free-solid-svg-icons';\nimport { play, stop } from '../services/playerService';\n\nconst PlayStopControl = ({ playing }) =>\n playing ? (\n <button className=\"btn btn-danger\" onClick={stop}>\n <FontAwesomeIcon icon={faStop} />\n </button>\n ) : (\n <button className=\"btn btn-dark\" onClick={play}>\n <FontAwesomeIcon icon={faPlay} />\n </button>\n );\n\nexport default PlayStopControl;\n","import React from 'react';\nimport PlayStopControl from './playStopControl';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faVolumeDown, faVolumeUp } from '@fortawesome/free-solid-svg-icons';\nimport { toast } from 'react-toastify';\nimport { withTranslation } from 'react-i18next';\nimport { changeVol } from '../services/playerService';\nimport { volTimeout } from '../config.json';\nimport './playerControls.scss';\n\nconst PlayerControls = ({ playerStatus, t }) => {\n const volDisabled = playerStatus.vol < 0;\n\n const handleVolChange = async delta => {\n const { data: vol } = await changeVol(delta);\n\n const toastId = 'vol';\n const toastMsg = `${t('volume')}: ${vol}%`;\n const toastOpts = { toastId, autoClose: volTimeout };\n if (toast.isActive(toastId)) toast.update(toastId, { render: toastMsg });\n else toast.info(toastMsg, toastOpts);\n };\n\n const renderVolButton = (delta, icon) => (\n <button\n className=\"btn btn-dark p-3\"\n disabled={volDisabled}\n onClick={() => handleVolChange(delta)}\n >\n <FontAwesomeIcon icon={icon} />\n </button>\n );\n\n return (\n <div className=\"player-controls btn-group w-100\">\n {renderVolButton('-5', faVolumeDown)}\n {renderVolButton('+5', faVolumeUp)}\n <PlayStopControl playing={playerStatus.playing} />\n </div>\n );\n};\n\nexport default withTranslation()(PlayerControls);\n","import React, { useEffect } from 'react';\nimport PlayerStatus from './playerStatus';\nimport PlayerControls from './playerControls';\nimport './player.scss';\n\nconst Player = ({ playerStatus }) => {\n useEffect(positionPlayerTop, [playerStatus]);\n\n return (\n <div className=\"player p-2\">\n <PlayerStatus playerStatus={playerStatus} />\n <PlayerControls playerStatus={playerStatus} />\n </div>\n );\n};\n\n// Calculate and set top for Player\n// Needed for desktop view with sticky\nconst positionPlayerTop = () => {\n const navHeight = document.querySelector('nav').clientHeight;\n const topMargin = 24;\n document.querySelector('.player').style.top = navHeight + topMargin + 'px';\n};\n\nexport default Player;\n","import React from 'react';\nimport { withTranslation } from 'react-i18next';\n\nconst SearchBox = ({ value, onChange, t }) => {\n return (\n <input\n className=\"form-control mb-4\"\n type=\"text\"\n id=\"query\"\n placeholder={t('search')}\n autoComplete=\"off\"\n value={value}\n onChange={e => onChange(e.target.value)}\n onFocus={() => onChange('')}\n ></input>\n );\n};\n\nexport default withTranslation()(SearchBox);\n","import React from 'react';\nimport './loader.scss';\n\nconst Loader = () => (\n <div className=\"loader\">\n <div className=\"spinner-border\" role=\"status\">\n <span className=\"sr-only\">Loading...</span>\n </div>\n </div>\n);\n\nexport default Loader;\n","import http from './httpService';\n\nconst apiEndpoint = `${process.env.REACT_APP_API_URL}/streams`;\n\nexport function getStreams() {\n return http.get(apiEndpoint);\n}\n","import React, { Component } from 'react';\nimport SearchBox from './common/searchBox';\nimport Loader from './loader';\nimport { withTranslation } from 'react-i18next';\nimport { toast } from 'react-toastify';\nimport { playRadio } from '../services/playerService';\nimport { getStreams } from '../services/streamsService';\nimport './streams.scss';\n\nclass Streams extends Component {\n state = { streams: {}, loading: true, query: '' };\n\n async componentDidMount() {\n const { data: streams } = await getStreams();\n this.setState({ streams, loading: false });\n }\n\n isPlaying = name => {\n return (\n name === this.props.playerStatus.title && this.props.playerStatus.playing\n );\n };\n\n handleItemClick = async name => {\n const { t, onBackdrop } = this.props;\n\n if (this.isPlaying(name)) return;\n\n onBackdrop(t('tunning'), name);\n try {\n await playRadio(name);\n } catch (ex) {\n if (ex.response && ex.response.status === 400)\n toast.error(t('errorNotFound'));\n }\n };\n\n getItemClasses = name => {\n const classes = 'streams__item ellipsis list-group-item ';\n return this.isPlaying(name)\n ? classes + 'active'\n : classes + 'list-group-item-action';\n };\n\n handleSearch = query => {\n this.setState({ query });\n };\n\n filteredStreams() {\n const { streams, query } = this.state;\n\n if (query === '') return streams;\n\n let filtered = {};\n for (let k in streams) {\n if (k.toLowerCase().includes(query.toLowerCase()) && !streams[k]) {\n filtered[k] = streams[k];\n }\n }\n return filtered;\n }\n\n renderList() {\n const streams = this.filteredStreams();\n\n if (Object.keys(streams).length === 0)\n return <h4 className=\"p-4\">{this.props.t('noStreams')}</h4>;\n\n return (\n <ul className=\"list-group list-group-flush\">\n {Object.keys(streams).map(name =>\n streams[name] ? (\n <li className=\"streams__header ellipsis\" key={name}>\n {name}\n </li>\n ) : (\n <li\n className={this.getItemClasses(name)}\n key={name}\n onClick={() => this.handleItemClick(name)}\n >\n {name}\n </li>\n )\n )}\n </ul>\n );\n }\n\n render() {\n if (this.state.loading) return <Loader />;\n\n return (\n <div className=\"streams p-2\">\n <SearchBox value={this.state.query} onChange={this.handleSearch} />\n {this.renderList()}\n </div>\n );\n }\n}\n\nexport default withTranslation()(Streams);\n","import React from 'react';\nimport PlayStopControl from './playStopControl';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faChevronUp } from '@fortawesome/free-solid-svg-icons';\nimport './miniPlayer.scss';\n\nconst MiniPlayer = ({ playerStatus }) => {\n const { title, playing } = playerStatus;\n return (\n <div className=\"mini-player p-3\">\n <div className=\"mini-player__left ellipsis pr-3\">\n <FontAwesomeIcon icon={faChevronUp} className=\"drawer__toggler mr-3\" />\n <span>{title}</span>\n </div>\n <PlayStopControl playing={playing} />\n </div>\n );\n};\n\nexport default MiniPlayer;\n","import React, { Component } from 'react';\nimport MiniPlayer from './miniPlayer';\nimport Player from './player';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faChevronDown } from '@fortawesome/free-solid-svg-icons';\nimport './drawer.scss';\n\nclass Drawer extends Component {\n state = { open: false };\n\n toggle = () => {\n this.setState({ open: !this.state.open });\n };\n\n handleClick = ({ target }) => {\n const blackList = target.tagName === 'BUTTON' || target.tagName === 'path';\n if (blackList) return;\n\n console.log('tag: ', target.tagName);\n console.log('class: ', target.className);\n this.toggle();\n };\n\n handleTouchStart = e => {\n this.setState({ touchStartY: e.touches[0].clientY });\n };\n\n handleTouchMove = e => {\n const { open, touchStartY } = this.state;\n const touchEndY = e.changedTouches[0].clientY;\n const touchUp = touchEndY < touchStartY;\n const touchDown = touchEndY > touchStartY;\n\n if (touchDown && open) this.toggle();\n else if (touchUp && !open) this.toggle();\n };\n\n render() {\n const { playerStatus } = this.props;\n let classes =\n 'drawer fixed-bottom bg-secondary border-top border-secondary';\n\n if (this.state.open) {\n classes += ' drawer--open';\n document.body.classList.add('body--drawer');\n } else document.body.classList.remove('body--drawer');\n\n return (\n <div\n className={classes}\n onClick={this.handleClick}\n onTouchStart={this.handleTouchStart}\n onTouchMove={this.handleTouchMove}\n >\n {this.state.open ? (\n <React.Fragment>\n <FontAwesomeIcon\n icon={faChevronDown}\n className=\"drawer__toggler fa-lg mb-3\"\n />\n <Player playerStatus={playerStatus} />{' '}\n </React.Fragment>\n ) : (\n <MiniPlayer playerStatus={playerStatus} />\n )}\n </div>\n );\n }\n}\n\nexport default Drawer;\n","import React from 'react';\n\nconst Modal = ({ id, title, footer, children }) => {\n return (\n <div\n className=\"modal fade\"\n id={id}\n tabIndex=\"-1\"\n role=\"dialog\"\n aria-labelledby=\"staticBackdropLabel\"\n aria-hidden=\"true\"\n >\n <div className=\"modal-dialog\" role=\"document\">\n <div className=\"modal-content\">\n <div className=\"modal-header\">\n <h5 className=\"modal-title\" id=\"staticBackdropLabel\">\n {title}\n </h5>\n <button\n type=\"button\"\n className=\"close\"\n data-dismiss=\"modal\"\n aria-label=\"Close\"\n >\n <span aria-hidden=\"true\">&times;</span>\n </button>\n </div>\n <div className=\"modal-body\">{children}</div>\n <div className=\"modal-footer\">{footer}</div>\n </div>\n </div>\n </div>\n );\n};\n\nexport default Modal;\n","const DEFAULT_THEME = 'darkly';\n\nconst STORAGE_KEY = 'theme';\n\nconst getThemePath = themeId =>\n `https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/${themeId}/bootstrap.min.css`;\n\nexport const themes = [\n { id: 'darkly', name: 'Darkly', themeColor: '#375a7f' },\n { id: 'lux', name: 'Lux', themeColor: '#1a1a1a' },\n { id: 'litera', name: 'Litera', themeColor: '#4582ec' }\n];\n\nexport function changeTheme(themeId) {\n localStorage.setItem(STORAGE_KEY, themeId);\n applyTheme();\n}\n\nexport function applyTheme() {\n const themeId = getThemeId();\n const themePath = getThemePath(themeId);\n const themeColor = themes.find(t => t.id === themeId).themeColor;\n\n const linkEl = document.querySelector('link[title=\"theme\"]');\n linkEl.setAttribute('href', themePath);\n\n const metaEl = document.querySelector('meta[name=\"theme-color\"]');\n metaEl.setAttribute('content', themeColor);\n}\n\nexport function getThemeId() {\n const localId = localStorage.getItem(STORAGE_KEY);\n\n if (localId === '') return DEFAULT_THEME;\n if (themes.filter(t => t.id === localId).length === 0) {\n localStorage.removeItem(STORAGE_KEY);\n return DEFAULT_THEME;\n }\n\n return localId;\n}\n","import React from 'react';\n\nconst Select = ({ id, label, row, data, ...rest }) => (\n <div className={row ? 'form-group row' : 'form-group'}>\n <label htmlFor={id} className=\"col-sm-2 col-form-label\">\n {label}\n </label>\n <div className=\"col-sm-10\">\n <select id={id} className=\"form-control\" {...rest}>\n {data.map(d => (\n <option key={d.id || d} value={d.id || d}>\n {d.name || d}\n </option>\n ))}\n </select>\n </div>\n </div>\n);\n\nexport default Select;\n","import React from 'react';\nimport Modal from './common/modal';\nimport { getThemeId, changeTheme, themes } from '../theme';\nimport i18next from 'i18next';\nimport { withTranslation } from 'react-i18next';\nimport { languages } from '../config.json';\nimport Select from './common/select';\n\nconst Settings = ({ t }) => {\n const renderFooter = () => (\n <button className=\"btn btn-secondary\" data-dismiss=\"modal\">\n {t('close')}\n </button>\n );\n\n return (\n <Modal id=\"settings\" title={t('settings')} footer={renderFooter()}>\n <Select\n id=\"theme-select\"\n label={t('theme')}\n data={themes}\n row\n value={getThemeId()}\n onChange={e => {\n changeTheme(e.target.value);\n }}\n />\n <Select\n id=\"language-select\"\n label={t('language')}\n data={languages}\n row\n value={i18next.language}\n onChange={e => {\n i18next.changeLanguage(e.target.value);\n }}\n />\n <hr className=\"mt-5\" />\n <p className=\"small\">\n Copyright &copy; 2017-2019&nbsp;\n <a\n href=\"https://rafaelc.org/\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Rafael Cavalcanti\n </a>\n </p>\n </Modal>\n );\n};\n\nexport default withTranslation()(Settings);\n","import React, { useState } from 'react';\nimport Modal from './common/modal';\nimport { toast } from 'react-toastify';\nimport { playURL } from '../services/playerService';\nimport { withTranslation } from 'react-i18next';\n\nconst URLDialog = ({ t }) => {\n const [url, setURL] = useState('');\n\n const handleOK = () => {\n if (url === '') return;\n doPlayURL(url);\n setURL('');\n };\n\n const doPlayURL = async url => {\n const errorToastOpts = {\n type: toast.TYPE.ERROR,\n render: t('errorNotFound')\n };\n\n const toastId = toast(t('tryingURL'));\n\n try {\n await playURL(url);\n } catch (ex) {\n if (ex.response && ex.response.status === 400)\n toast.update(toastId, errorToastOpts);\n }\n };\n\n const handleChange = ({ target: input }) => {\n setURL(input.value);\n };\n\n const handleKeyDown = e => {\n if (e.key === 'Enter') handleOK();\n };\n\n const renderFooter = () => (\n <React.Fragment>\n <button\n className=\"btn btn-primary\"\n data-dismiss=\"modal\"\n onClick={handleOK}\n >\n OK\n </button>\n <button\n className=\"btn btn-secondary\"\n onClick={() => setURL('')}\n data-dismiss=\"modal\"\n >\n Cancel\n </button>\n </React.Fragment>\n );\n\n return (\n <Modal id=\"url-dialog\" title={t('playURL')} footer={renderFooter()}>\n <input\n className=\"form-control mb-4\"\n type=\"text\"\n placeholder=\"URL\"\n value={url}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n />\n </Modal>\n );\n};\n\nexport default withTranslation()(URLDialog);\n","import React, { useState, useEffect } from 'react';\nimport './backdrop.scss';\n\nconst Backdrop = ({ title, body }) => {\n const [bodyStyled, setBodyStyled] = useState(false);\n\n const getScrollbarWidth = () =>\n window.innerWidth - document.documentElement.clientWidth;\n\n const doBodyStyle = () => {\n if (title) {\n // Calculate this BEFORE adding the class.\n document.body.style.paddingRight = getScrollbarWidth() + 'px';\n document.body.classList.add('body--backdrop');\n setBodyStyled(true);\n // We need this check because Bootstrap modals also style paddingRight\n } else if (!title && bodyStyled) {\n document.body.style.paddingRight = 0;\n document.body.classList.remove('body--backdrop');\n setBodyStyled(false);\n }\n };\n\n useEffect(doBodyStyle, [title, body]);\n\n const classes =\n 'backdrop p-2 text-white' + (title ? ' backdrop--visible' : '');\n\n // We need text-white on h* tags because some themes override it\n return (\n <div className={classes}>\n <h3 className=\"text-white\">{title}</h3>\n <h5 className=\"text-white\">{body}</h5>\n </div>\n );\n};\n\nexport default Backdrop;\n","import React, { Component } from 'react';\nimport NavBar from './components/navBar';\nimport Player from './components/player';\nimport Streams from './components/streams';\nimport Drawer from './components/drawer';\nimport Settings from './components/settings';\nimport URLDialog from './components/urlDialog';\nimport Loader from './components/loader';\nimport Backdrop from './components/backdrop';\nimport { ToastContainer } from 'react-toastify';\nimport { getStatus } from './services/playerService';\nimport { withTranslation } from 'react-i18next';\nimport { updateInterval, backdropTimeout } from './config.json';\nimport './App.scss';\nimport 'react-toastify/dist/ReactToastify.css';\n\nclass App extends Component {\n state = {\n playerStatus: {},\n loading: true,\n networkError: false,\n backdrop: {}\n };\n\n componentDidMount() {\n this.updatePlayerStatus();\n }\n\n async updatePlayerStatus() {\n try {\n const { data: playerStatus } = await getStatus();\n this.setState({ playerStatus, loading: false, networkError: false });\n } catch (ex) {\n this.setState({ loading: false, networkError: true });\n }\n\n setTimeout(() => this.updatePlayerStatus(), updateInterval);\n }\n\n handleBackdrop = (title, body = '') => {\n this.setState({ backdrop: { title, body } });\n setTimeout(() => this.setState({ backdrop: {} }), backdropTimeout);\n };\n\n render() {\n const { loading, networkError, backdrop, playerStatus } = this.state;\n const { t } = this.props;\n\n if (networkError) return <Backdrop title={t('errorNetwork')} />;\n if (loading) return <Loader />;\n if (!playerStatus.con_mpd) return <Backdrop title={t('disconnectedMPD')} />;\n\n return (\n <div className=\"app\">\n <ToastContainer />\n <Backdrop title={backdrop.title} body={backdrop.body} />\n <NavBar />\n <Drawer playerStatus={playerStatus} />\n\n <main className=\"app-main p-4\">\n <Player playerStatus={playerStatus} />\n <Streams\n onBackdrop={this.handleBackdrop}\n playerStatus={playerStatus}\n />\n <Settings />\n <URLDialog />\n </main>\n </div>\n );\n }\n}\n\nexport default withTranslation()(App);\n","// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on subsequent visits to a page, after all the\n// existing tabs open on the page have been closed, since previously cached\n// resources are updated in the background.\n\n// To learn more about the benefits of this model and instructions on how to\n// opt-in, read https://bit.ly/CRA-PWA\n\nconst isLocalhost = Boolean(\n window.location.hostname === 'localhost' ||\n // [::1] is the IPv6 localhost address.\n window.location.hostname === '[::1]' ||\n // 127.0.0.0/8 are considered localhost for IPv4.\n window.location.hostname.match(\n /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n )\n);\n\nexport function register(config) {\n if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n // The URL constructor is available in all browsers that support SW.\n const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);\n if (publicUrl.origin !== window.location.origin) {\n // Our service worker won't work if PUBLIC_URL is on a different origin\n // from what our page is served on. This might happen if a CDN is used to\n // serve assets; see https://github.com/facebook/create-react-app/issues/2374\n return;\n }\n\n window.addEventListener('load', () => {\n const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n if (isLocalhost) {\n // This is running on localhost. Let's check if a service worker still exists or not.\n checkValidServiceWorker(swUrl, config);\n\n // Add some additional logging to localhost, pointing developers to the\n // service worker/PWA documentation.\n navigator.serviceWorker.ready.then(() => {\n console.log(\n 'This web app is being served cache-first by a service ' +\n 'worker. To learn more, visit https://bit.ly/CRA-PWA'\n );\n });\n } else {\n // Is not localhost. Just register service worker\n registerValidSW(swUrl, config);\n }\n });\n }\n}\n\nfunction registerValidSW(swUrl, config) {\n navigator.serviceWorker\n .register(swUrl)\n .then(registration => {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing;\n if (installingWorker == null) {\n return;\n }\n installingWorker.onstatechange = () => {\n if (installingWorker.state === 'installed') {\n if (navigator.serviceWorker.controller) {\n // At this point, the updated precached content has been fetched,\n // but the previous service worker will still serve the older\n // content until all client tabs are closed.\n console.log(\n 'New content is available and will be used when all ' +\n 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'\n );\n\n // Execute callback\n if (config && config.onUpdate) {\n config.onUpdate(registration);\n }\n } else {\n // At this point, everything has been precached.\n // It's the perfect time to display a\n // \"Content is cached for offline use.\" message.\n console.log('Content is cached for offline use.');\n\n // Execute callback\n if (config && config.onSuccess) {\n config.onSuccess(registration);\n }\n }\n }\n };\n };\n })\n .catch(error => {\n console.error('Error during service worker registration:', error);\n });\n}\n\nfunction checkValidServiceWorker(swUrl, config) {\n // Check if the service worker can be found. If it can't reload the page.\n fetch(swUrl, {\n headers: { 'Service-Worker': 'script' }\n })\n .then(response => {\n // Ensure service worker exists, and that we really are getting a JS file.\n const contentType = response.headers.get('content-type');\n if (\n response.status === 404 ||\n (contentType != null && contentType.indexOf('javascript') === -1)\n ) {\n // No service worker found. Probably a different app. Reload the page.\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister().then(() => {\n window.location.reload();\n });\n });\n } else {\n // Service worker found. Proceed as normal.\n registerValidSW(swUrl, config);\n }\n })\n .catch(() => {\n console.log(\n 'No internet connection found. App is running in offline mode.'\n );\n });\n}\n\nexport function unregister() {\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister();\n });\n }\n}\n","import i18n from 'i18next';\nimport { initReactI18next } from 'react-i18next';\n\nimport Backend from 'i18next-xhr-backend';\nimport LanguageDetector from 'i18next-browser-languagedetector';\n// not like to use this?\n// have a look at the Quick start guide\n// for passing in lng and translations on init\n\ni18n\n // load translation using xhr -> see /public/locales\n // learn more: https://github.com/i18next/i18next-xhr-backend\n .use(Backend)\n // detect user language\n // learn more: https://github.com/i18next/i18next-browser-languageDetector\n .use(LanguageDetector)\n // pass the i18n instance to react-i18next.\n .use(initReactI18next)\n // init i18next\n // for all options read: https://www.i18next.com/overview/configuration-options\n .init({\n fallbackLng: 'en',\n debug: process.env.REACT_APP_I18N_DEBUG,\n\n interpolation: {\n escapeValue: false // not needed for react as it escapes by default\n }\n });\n\nexport default i18n;\n","import React, { Suspense } from 'react';\nimport ReactDOM from 'react-dom';\nimport App from './App';\nimport * as serviceWorker from './serviceWorker';\nimport Loader from './components/loader';\nimport './i18n';\nimport { applyTheme } from './theme';\nimport './index.scss';\n\n// Bootstrap\n//import 'bootstrap/dist/css/bootstrap.css';\n//import 'bootswatch/dist/flatly/bootstrap.min.css'; // This replaces default Bootstrap file\nimport 'jquery/dist/jquery.min.js';\nimport 'bootstrap/dist/js/bootstrap.js';\n\napplyTheme();\n\nReactDOM.render(\n // Suspense needed for i18n\n <Suspense fallback={<Loader />}>\n <App />\n </Suspense>,\n document.getElementById('root')\n);\n\n// If you want your app to work offline and load faster, you can change\n// unregister() to register() below. Note this comes with some pitfalls.\n// Learn more about service workers: https://bit.ly/CRA-PWA\nserviceWorker.unregister();\n"],"sourceRoot":""}
data/lib/pifi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module PiFi
2
- VERSION = "0.3.2".freeze
2
+ VERSION = "0.3.3".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pifi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rafael Cavalcanti
@@ -122,7 +122,7 @@ files:
122
122
  - lib/pifi/public/mstile-310x150.png
123
123
  - lib/pifi/public/mstile-310x310.png
124
124
  - lib/pifi/public/mstile-70x70.png
125
- - lib/pifi/public/precache-manifest.b35a62c41b1e6f7d7178e760c633328f.js
125
+ - lib/pifi/public/precache-manifest.5a9bc068aedf916dd7ceb2986c13099f.js
126
126
  - lib/pifi/public/robots.txt
127
127
  - lib/pifi/public/safari-pinned-tab.svg
128
128
  - lib/pifi/public/service-worker.js
@@ -133,14 +133,12 @@ files:
133
133
  - lib/pifi/public/static/js/2.2b057ab5.chunk.js
134
134
  - lib/pifi/public/static/js/2.2b057ab5.chunk.js.LICENSE
135
135
  - lib/pifi/public/static/js/2.2b057ab5.chunk.js.map
136
- - lib/pifi/public/static/js/main.1ea4089a.chunk.js
137
- - lib/pifi/public/static/js/main.1ea4089a.chunk.js.map
136
+ - lib/pifi/public/static/js/main.f588df7b.chunk.js
137
+ - lib/pifi/public/static/js/main.f588df7b.chunk.js.map
138
138
  - lib/pifi/public/static/js/runtime-main.f04a0f25.js
139
139
  - lib/pifi/public/static/js/runtime-main.f04a0f25.js.map
140
140
  - lib/pifi/public/static/media/logo.91554ce9.svg
141
141
  - lib/pifi/version.rb
142
- - lib/pifi/views/index.erb
143
- - lib/pifi/views/layout.erb
144
142
  homepage: https://github.com/rccavalcanti/pifi-radio
145
143
  licenses:
146
144
  - GPL-3.0-only
@@ -1 +0,0 @@
1
- {"version":3,"sources":["logo.svg","components/navBar.jsx","components/playerStatus.jsx","services/httpService.js","services/playerService.js","components/playStopControl.jsx","components/playerControls.jsx","components/player.jsx","components/common/searchBox.jsx","components/loader.jsx","services/streamsService.js","components/streams.jsx","components/miniPlayer.jsx","components/drawer.jsx","components/common/modal.jsx","theme.js","components/common/select.jsx","components/settings.jsx","components/urlDialog.jsx","components/backdrop.jsx","App.js","serviceWorker.js","i18n.js","index.js"],"names":["module","exports","withTranslation","t","className","href","src","logo","width","height","alt","type","data-toggle","data-target","aria-controls","aria-expanded","aria-label","id","playerStatus","playing","title","axios","interceptors","response","use","error","expectedError","status","networkError","toast","i18n","Promise","reject","get","post","apiEndpoint","process","body","method","params","FormData","set","play","http","stop","changeVol","delta","playRadio","name","playURL","url","PlayStopControl","onClick","icon","faStop","faPlay","volDisabled","vol","renderVolButton","disabled","a","async","data","toastMsg","toastOpts","toastId","autoClose","volTimeout","isActive","update","render","info","handleVolChange","faVolumeDown","faVolumeUp","positionPlayerTop","navHeight","document","querySelector","clientHeight","style","top","Player","useEffect","value","onChange","placeholder","autoComplete","e","target","onFocus","Loader","role","Streams","state","streams","loading","query","isPlaying","props","handleItemClick","onBackdrop","getItemClasses","classes","handleSearch","setState","this","filtered","k","toLowerCase","includes","filteredStreams","Object","keys","length","map","key","renderList","Component","MiniPlayer","faChevronUp","Drawer","open","toggle","handleClick","tagName","console","log","handleTouchStart","touchStartY","touches","clientY","handleTouchMove","touchEndY","changedTouches","touchUp","classList","add","remove","onTouchStart","onTouchMove","Fragment","faChevronDown","Modal","footer","children","tabIndex","aria-labelledby","aria-hidden","data-dismiss","DEFAULT_THEME","STORAGE_KEY","getThemePath","themeId","themes","themeColor","applyTheme","getThemeId","themePath","find","setAttribute","localId","localStorage","getItem","filter","removeItem","Select","label","row","rest","htmlFor","d","setItem","languages","i18next","language","changeLanguage","useState","setURL","handleOK","doPlayURL","errorToastOpts","TYPE","ERROR","input","onKeyDown","Backdrop","bodyStyled","setBodyStyled","paddingRight","window","innerWidth","documentElement","clientWidth","App","backdrop","handleBackdrop","setTimeout","backdropTimeout","updatePlayerStatus","updateInterval","con_mpd","Boolean","location","hostname","match","Backend","LanguageDetector","initReactI18next","init","fallbackLng","debug","REACT_APP_I18N_DEBUG","interpolation","escapeValue","ReactDOM","fallback","getElementById","navigator","serviceWorker","ready","then","registration","unregister"],"mappings":"0MAAAA,EAAOC,QAAU,IAA0B,kC,uZCmD5BC,iBA9CA,SAAC,GAAD,IAAGC,EAAH,EAAGA,EAAH,OACb,yBAAKC,UAAU,mDACb,uBAAGA,UAAU,eAAeC,KAAK,KAC/B,yBACEC,IAAKC,IACLC,MAAM,KACNC,OAAO,KACPL,UAAU,wCACVM,IAAI,KANR,cAUA,4BACEN,UAAU,iBACVO,KAAK,SACLC,cAAY,WACZC,cAAY,sBACZC,gBAAc,qBACdC,gBAAc,QACdC,aAAW,qBAEX,0BAAMZ,UAAU,yBAElB,yBAAKA,UAAU,2BAA2Ba,GAAG,sBAC3C,yBAAKb,UAAU,wBAGb,4BACEA,UAAU,wBACVQ,cAAY,QACZC,cAAY,eAEXV,EAAE,YAGL,4BACEC,UAAU,wBACVQ,cAAY,QACZC,cAAY,aAEXV,EAAE,mBC7BED,iBAbM,SAAC,GAAyB,IAAvBgB,EAAsB,EAAtBA,aAAcf,EAAQ,EAARA,EAC5BgB,EAAmBD,EAAnBC,QAASC,EAAUF,EAAVE,MAEjB,OACE,yBAAKhB,UAAU,eACb,wBAAIA,UAAU,wBACDD,EAAVgB,EAAY,UAAe,YAE9B,wBAAIf,UAAU,YAAYgB,O,wCCNhCC,IAAMC,aAAaC,SAASC,IAAI,MAAM,SAAAC,GACpC,IAAMC,EACJD,EAAMF,UACNE,EAAMF,SAASI,QAAU,KACzBF,EAAMF,SAASI,OAAS,IAGpBC,GAAgBH,EAAMF,SAgB5B,OAbKG,GAAkBE,GACrBC,IAAMJ,MACJ,kBAAC,IAAD,MACG,SAACtB,EAAD,KAAM2B,KAAN,OAAiB,2BAAI3B,EAAE,wBAK1BsB,EAAMF,UAAsC,MAA1BE,EAAMF,SAASI,QACnCE,IAAMJ,MACJ,kBAAC,IAAD,MAAc,SAACtB,EAAD,KAAM2B,KAAN,OAAiB,2BAAI3B,EAAE,uBAGlC4B,QAAQC,OAAOP,MAGT,OACbQ,IAAKZ,IAAMY,IACXC,KAAMb,IAAMa,MC/BRC,EAAW,UAAMC,OAAN,WAEjB,SAASC,EAAKC,GAAwB,IAAhBC,EAAe,uDAAN,KACvBF,EAAO,IAAIG,SAGjB,OAFAH,EAAKI,IAAI,SAAUH,GACfC,GAAQF,EAAKI,IAAI,SAAUF,GACxBF,EAOF,SAASK,IACd,OAAOC,EAAKT,KAAKC,EAAaE,EAAK,SAG9B,SAASO,IACd,OAAOD,EAAKT,KAAKC,EAAaE,EAAK,SAG9B,SAASQ,EAAUC,GACxB,OAAOH,EAAKT,KAAKC,EAAaE,EAAK,aAAcS,IAG5C,SAASC,EAAUC,GACxB,OAAOL,EAAKT,KAAKC,EAAaE,EAAK,cAAeW,IAG7C,SAASC,EAAQC,GACtB,OAAOP,EAAKT,KAAKC,EAAaE,EAAK,YAAaa,IC3BlD,IAWeC,EAXS,SAAC,GAAD,SAAGhC,QAEvB,4BAAQf,UAAU,iBAAiBgD,QAASR,GAC1C,kBAAC,IAAD,CAAiBS,KAAMC,OAGzB,4BAAQlD,UAAU,eAAegD,QAASV,GACxC,kBAAC,IAAD,CAAiBW,KAAME,Q,QC8BdrD,G,MAAAA,eAhCQ,SAAC,GAAyB,IAAvBgB,EAAsB,EAAtBA,aAAcf,EAAQ,EAARA,EAChCqD,EAActC,EAAauC,IAAM,EAYjCC,EAAkB,SAACZ,EAAOO,GAAR,OACtB,4BACEjD,UAAU,mBACVuD,SAAUH,EACVJ,QAAS,kBAdW,SAAMN,GAAN,qBAAAc,EAAAC,OAAA,kEAAAD,EAAA,MACMf,EAAUC,IADhB,gBACRW,EADQ,EACdK,KAEQ,MACVC,EAJgB,UAIF5D,EAAE,UAJA,aAIcsD,EAJd,KAKhBO,EAAY,CAAEC,QAFJ,MAEaC,UAAWC,KACpCtC,IAAMuC,SAHM,OAGavC,IAAMwC,OAHnB,MAGmC,CAAEC,OAAQP,IACxDlC,IAAM0C,KAAKR,EAAUC,GAPJ,qCAcLQ,CAAgB1B,KAE/B,kBAAC,IAAD,CAAiBO,KAAMA,MAI3B,OACE,yBAAKjD,UAAU,mCACZsD,EAAgB,KAAMe,KACtBf,EAAgB,KAAMgB,KACvB,kBAAC,EAAD,CAAiBvD,QAASD,EAAaC,eCnBvCwD,G,MAAoB,WACxB,IAAMC,EAAYC,SAASC,cAAc,OAAOC,aAEhDF,SAASC,cAAc,WAAWE,MAAMC,IAAML,EAD5B,GACoD,OAGzDM,EAnBA,SAAC,GAAsB,IAApBhE,EAAmB,EAAnBA,aAGhB,OAFAiE,oBAAUR,EAAmB,CAACzD,IAG5B,yBAAKd,UAAU,cACb,kBAAC,EAAD,CAAcc,aAAcA,IAC5B,kBAAC,EAAD,CAAgBA,aAAcA,MCOrBhB,iBAfG,SAAC,GAA4B,IAA1BkF,EAAyB,EAAzBA,MAAOC,EAAkB,EAAlBA,SAAUlF,EAAQ,EAARA,EACpC,OACE,2BACEC,UAAU,oBACVO,KAAK,OACLM,GAAG,QACHqE,YAAanF,EAAE,UACfoF,aAAa,MACbH,MAAOA,EACPC,SAAU,SAAAG,GAAC,OAAIH,EAASG,EAAEC,OAAOL,QACjCM,QAAS,kBAAML,EAAS,UCFfM,G,MARA,kBACb,yBAAKvF,UAAU,UACb,yBAAKA,UAAU,iBAAiBwF,KAAK,UACnC,0BAAMxF,UAAU,WAAhB,kBCJA+B,EAAW,UAAMC,OAAN,Y,UCOXyD,E,2MACJC,MAAQ,CAAEC,QAAS,GAAIC,SAAS,EAAMC,MAAO,I,EAO7CC,UAAY,SAAAlD,GACV,OACEA,IAAS,EAAKmD,MAAMjF,aAAaE,OAAS,EAAK+E,MAAMjF,aAAaC,S,EAItEiF,gBAAkB,SAAMpD,GAAN,mBAAAY,EAAAC,OAAA,qDACU,EAAKsC,MAAvBhG,EADQ,EACRA,EAAGkG,EADK,EACLA,YAEP,EAAKH,UAAUlD,GAHH,wDAKhBqD,EAAWlG,EAAE,WAAY6C,GALT,oBAAAY,EAAA,MAORb,EAAUC,IAPF,uDASV,KAAGzB,UAAmC,MAAvB,KAAGA,SAASI,QAC7BE,IAAMJ,MAAMtB,EAAE,kBAVF,yD,EAclBmG,eAAiB,SAAAtD,GACf,IAAMuD,EAAU,0CAChB,OAAO,EAAKL,UAAUlD,GAClBuD,EAAU,SACVA,EAAU,0B,EAGhBC,aAAe,SAAAP,GACb,EAAKQ,SAAS,CAAER,W,uLDxCXtD,EAAKV,IAAIE,I,gBCQA4D,E,EAANjC,KACR4C,KAAKD,SAAS,CAAEV,UAASC,SAAS,I,yFAkCjB,IAAD,EACWU,KAAKZ,MAAxBC,EADQ,EACRA,QAASE,EADD,EACCA,MAEjB,GAAc,KAAVA,EAAc,OAAOF,EAEzB,IAAIY,EAAW,GACf,IAAK,IAAIC,KAAKb,EACRa,EAAEC,cAAcC,SAASb,EAAMY,iBAAmBd,EAAQa,KAC5DD,EAASC,GAAKb,EAAQa,IAG1B,OAAOD,I,mCAGK,IAAD,OACLZ,EAAUW,KAAKK,kBAErB,OAAoC,IAAhCC,OAAOC,KAAKlB,GAASmB,OAChB,wBAAI9G,UAAU,OAAOsG,KAAKP,MAAMhG,EAAE,cAGzC,wBAAIC,UAAU,+BACX4G,OAAOC,KAAKlB,GAASoB,KAAI,SAAAnE,GAAI,OAC5B+C,EAAQ/C,GACN,wBAAI5C,UAAU,2BAA2BgH,IAAKpE,GAC3CA,GAGH,wBACE5C,UAAW,EAAKkG,eAAetD,GAC/BoE,IAAKpE,EACLI,QAAS,kBAAM,EAAKgD,gBAAgBpD,KAEnCA,S,+BASX,OAAI0D,KAAKZ,MAAME,QAAgB,kBAAC,EAAD,MAG7B,yBAAK5F,UAAU,eACb,kBAAC,EAAD,CAAWgF,MAAOsB,KAAKZ,MAAMG,MAAOZ,SAAUqB,KAAKF,eAClDE,KAAKW,kB,GAtFQC,aA4FPpH,gBAAkB2F,GClFlB0B,G,MAbI,SAAC,GAAsB,IAApBrG,EAAmB,EAAnBA,aACZE,EAAmBF,EAAnBE,MAAOD,EAAYD,EAAZC,QACf,OACE,yBAAKf,UAAU,mBACb,yBAAKA,UAAU,mCACb,kBAAC,IAAD,CAAiBiD,KAAMmE,IAAapH,UAAU,yBAC9C,8BAAOgB,IAET,kBAAC,EAAD,CAAiBD,QAASA,OCwDjBsG,G,iNA9Db3B,MAAQ,CAAE4B,MAAM,G,EAEhBC,OAAS,WACP,EAAKlB,SAAS,CAAEiB,MAAO,EAAK5B,MAAM4B,Q,EAGpCE,YAAc,YAAiB,IAAdnC,EAAa,EAAbA,OACsB,WAAnBA,EAAOoC,SAA2C,SAAnBpC,EAAOoC,UAGxDC,QAAQC,IAAI,QAAStC,EAAOoC,SAC5BC,QAAQC,IAAI,UAAWtC,EAAOrF,WAC9B,EAAKuH,W,EAGPK,iBAAmB,SAAAxC,GACjB,EAAKiB,SAAS,CAAEwB,YAAazC,EAAE0C,QAAQ,GAAGC,W,EAG5CC,gBAAkB,SAAA5C,GAAM,IAAD,EACS,EAAKM,MAA3B4B,EADa,EACbA,KAAMO,EADO,EACPA,YACRI,EAAY7C,EAAE8C,eAAe,GAAGH,QAChCI,EAAUF,EAAYJ,EACVI,EAAYJ,GAEbP,EAAM,EAAKC,SACnBY,IAAYb,GAAM,EAAKC,U,wEAGxB,IACAzG,EAAiBwF,KAAKP,MAAtBjF,aACJqF,EACF,+DAOF,OALIG,KAAKZ,MAAM4B,MACbnB,GAAW,gBACX1B,SAASxC,KAAKmG,UAAUC,IAAI,iBACvB5D,SAASxC,KAAKmG,UAAUE,OAAO,gBAGpC,yBACEtI,UAAWmG,EACXnD,QAASsD,KAAKkB,YACde,aAAcjC,KAAKsB,iBACnBY,YAAalC,KAAK0B,iBAEjB1B,KAAKZ,MAAM4B,KACV,kBAAC,IAAMmB,SAAP,KACE,kBAAC,IAAD,CACExF,KAAMyF,IACN1I,UAAU,+BAEZ,kBAAC,EAAD,CAAQc,aAAcA,IAAiB,KAGzC,kBAAC,EAAD,CAAYA,aAAcA,S,GAxDfoG,cC4BNyB,EAjCD,SAAC,GAAqC,IAAnC9H,EAAkC,EAAlCA,GAAIG,EAA8B,EAA9BA,MAAO4H,EAAuB,EAAvBA,OAAQC,EAAe,EAAfA,SAClC,OACE,yBACE7I,UAAU,aACVa,GAAIA,EACJiI,SAAS,KACTtD,KAAK,SACLuD,kBAAgB,sBAChBC,cAAY,QAEZ,yBAAKhJ,UAAU,eAAewF,KAAK,YACjC,yBAAKxF,UAAU,iBACb,yBAAKA,UAAU,gBACb,wBAAIA,UAAU,cAAca,GAAG,uBAC5BG,GAEH,4BACET,KAAK,SACLP,UAAU,QACViJ,eAAa,QACbrI,aAAW,SAEX,0BAAMoI,cAAY,QAAlB,UAGJ,yBAAKhJ,UAAU,cAAc6I,GAC7B,yBAAK7I,UAAU,gBAAgB4I,OC5BnCM,EAAgB,SAEhBC,EAAc,QAEdC,EAAe,SAAAC,GAAO,oEAC6BA,EAD7B,uBAGfC,EAAS,CACpB,CAAEzI,GAAI,SAAU+B,KAAM,SAAU2G,WAAY,WAC5C,CAAE1I,GAAI,MAAO+B,KAAM,MAAO2G,WAAY,WACtC,CAAE1I,GAAI,SAAU+B,KAAM,SAAU2G,WAAY,YAQvC,SAASC,IACd,IAAMH,EAAUI,IACVC,EAAYN,EAAaC,GACzBE,EAAaD,EAAOK,MAAK,SAAA5J,GAAC,OAAIA,EAAEc,KAAOwI,KAASE,WAEvC9E,SAASC,cAAc,uBAC/BkF,aAAa,OAAQF,GAEbjF,SAASC,cAAc,4BAC/BkF,aAAa,UAAWL,GAG1B,SAASE,IACd,IAAMI,EAAUC,aAAaC,QAAQZ,GAErC,MAAgB,KAAZU,EAAuBX,EACyB,IAAhDI,EAAOU,QAAO,SAAAjK,GAAC,OAAIA,EAAEc,KAAOgJ,KAAS/C,QACvCgD,aAAaG,WAAWd,GACjBD,GAGFW,E,oBCpBMK,EAjBA,SAAC,GAAD,IAAGrJ,EAAH,EAAGA,GAAIsJ,EAAP,EAAOA,MAAOC,EAAd,EAAcA,IAAK1G,EAAnB,EAAmBA,KAAS2G,EAA5B,kDACb,yBAAKrK,UAAWoK,EAAM,iBAAmB,cACvC,2BAAOE,QAASzJ,EAAIb,UAAU,2BAC3BmK,GAEH,yBAAKnK,UAAU,aACb,0CAAQa,GAAIA,EAAIb,UAAU,gBAAmBqK,GAC1C3G,EAAKqD,KAAI,SAAAwD,GAAC,OACT,4BAAQvD,IAAKuD,EAAE1J,IAAM0J,EAAGvF,MAAOuF,EAAE1J,IAAM0J,GACpCA,EAAE3H,MAAQ2H,UC8BRzK,kBAjCE,SAAC,GAAW,IAATC,EAAQ,EAARA,EAOlB,OACE,kBAAC,EAAD,CAAOc,GAAG,WAAWG,MAAOjB,EAAE,YAAa6I,OAN3C,4BAAQ5I,UAAU,oBAAoBiJ,eAAa,SAChDlJ,EAAE,WAMH,kBAAC,EAAD,CACEc,GAAG,eACHsJ,MAAOpK,EAAE,SACT2D,KAAM4F,EACNc,KAAG,EACHpF,MAAOyE,IACPxE,SAAU,SAAAG,GFVX,IAAqBiE,IEWNjE,EAAEC,OAAOL,MFV7B8E,aAAaU,QAAQrB,EAAaE,GAClCG,OEYI,kBAAC,EAAD,CACE3I,GAAG,kBACHsJ,MAAOpK,EAAE,YACT2D,KAAM+G,IACNL,KAAG,EACHpF,MAAO0F,IAAQC,SACf1F,SAAU,SAAAG,GACRsF,IAAQE,eAAexF,EAAEC,OAAOL,c,SCsC3BlF,kBAlEG,SAAC,GAAW,IAATC,EAAQ,EAARA,EAAQ,EACL8K,mBAAS,IADJ,oBACpB/H,EADoB,KACfgI,EADe,KAGrBC,EAAW,WACH,KAARjI,IACJkI,EAAUlI,GACVgI,EAAO,MAGHE,EAAY,SAAMlI,GAAN,iBAAAU,EAAAC,OAAA,uDACVwH,EAAiB,CACrB1K,KAAMkB,IAAMyJ,KAAKC,MACjBjH,OAAQnE,EAAE,kBAGN8D,EAAUpC,YAAM1B,EAAE,cANR,oBAAAyD,EAAA,MASRX,EAAQC,IATA,uDAWV,KAAG3B,UAAmC,MAAvB,KAAGA,SAASI,QAC7BE,IAAMwC,OAAOJ,EAASoH,GAZV,yDA2ClB,OACE,kBAAC,EAAD,CAAOpK,GAAG,aAAaG,MAAOjB,EAAE,WAAY6I,OAnB5C,kBAAC,IAAMH,SAAP,KACE,4BACEzI,UAAU,kBACViJ,eAAa,QACbjG,QAAS+H,GAHX,MAOA,4BACE/K,UAAU,oBACVgD,QAAS,kBAAM8H,EAAO,KACtB7B,eAAa,SAHf,YAYA,2BACEjJ,UAAU,oBACVO,KAAK,OACL2E,YAAY,MACZF,MAAOlC,EACPmC,SAlCe,SAAC,GAAuB,IAAbmG,EAAY,EAApB/F,OACtByF,EAAOM,EAAMpG,QAkCTqG,UA/BgB,SAAAjG,GACN,UAAVA,EAAE4B,KAAiB+D,WCCZO,I,MAlCE,SAAC,GAAqB,IAAnBtK,EAAkB,EAAlBA,MAAOiB,EAAW,EAAXA,KAAW,EACA4I,oBAAS,GADT,oBAC7BU,EAD6B,KACjBC,EADiB,KAoBpCzG,qBAdoB,WACd/D,GAEFyD,SAASxC,KAAK2C,MAAM6G,aALtBC,OAAOC,WAAalH,SAASmH,gBAAgBC,YAKc,KACzDpH,SAASxC,KAAKmG,UAAUC,IAAI,kBAC5BmD,GAAc,KAEJxK,GAASuK,IACnB9G,SAASxC,KAAK2C,MAAM6G,aAAe,EACnChH,SAASxC,KAAKmG,UAAUE,OAAO,kBAC/BkD,GAAc,MAIK,CAACxK,EAAOiB,IAE/B,IAAMkE,EACJ,2BAA6BnF,EAAQ,qBAAuB,IAG9D,OACE,yBAAKhB,UAAWmG,GACd,wBAAInG,UAAU,cAAcgB,GAC5B,wBAAIhB,UAAU,cAAciC,MChB5B6J,I,uNACJpG,MAAQ,CACN5E,aAAc,GACd8E,SAAS,EACTpE,cAAc,EACduK,SAAU,I,EAkBZC,eAAiB,SAAChL,GAAsB,IAAfiB,EAAc,uDAAP,GAC9B,EAAKoE,SAAS,CAAE0F,SAAU,CAAE/K,QAAOiB,UACnCgK,YAAW,kBAAM,EAAK5F,SAAS,CAAE0F,SAAU,OAAOG,M,mFAhBlD5F,KAAK6F,uB,+JhBbA5J,EAAKV,IAAIE,I,gBgBkBEjB,E,EAAN4C,KACR4C,KAAKD,SAAS,CAAEvF,eAAc8E,SAAS,EAAOpE,cAAc,I,gDAE5D8E,KAAKD,SAAS,CAAET,SAAS,EAAOpE,cAAc,I,QAGhDyK,YAAW,kBAAM,EAAKE,uBAAsBC,K,yFAQpC,IAAD,EACmD9F,KAAKZ,MAAvDE,EADD,EACCA,QAASpE,EADV,EACUA,aAAcuK,EADxB,EACwBA,SAAUjL,EADlC,EACkCA,aACjCf,EAAMuG,KAAKP,MAAXhG,EAER,OAAIyB,EAAqB,kBAAC,GAAD,CAAUR,MAAOjB,EAAE,kBACxC6F,EAAgB,kBAAC,EAAD,MACf9E,EAAauL,QAGhB,yBAAKrM,UAAU,OACb,kBAAC,IAAD,MACA,kBAAC,GAAD,CAAUgB,MAAO+K,EAAS/K,MAAOiB,KAAM8J,EAAS9J,OAChD,kBAAC,EAAD,MACA,kBAAC,EAAD,CAAQnB,aAAcA,IAEtB,0BAAMd,UAAU,gBACd,kBAAC,EAAD,CAAQc,aAAcA,IACtB,kBAAC,EAAD,CACEmF,WAAYK,KAAK0F,eACjBlL,aAAcA,IAEhB,kBAAC,GAAD,MACA,kBAAC,GAAD,QAhB4B,kBAAC,GAAD,CAAUE,MAAOjB,EAAE,yB,GAlCvCmH,cAyDHpH,iBAAkBgM,IC7DbQ,QACW,cAA7BZ,OAAOa,SAASC,UAEe,UAA7Bd,OAAOa,SAASC,UAEhBd,OAAOa,SAASC,SAASC,MACvB,2D,sBCTN/K,IAGGN,IAAIsL,MAGJtL,IAAIuL,MAEJvL,IAAIwL,KAGJC,KAAK,CACJC,YAAa,KACbC,MAAO/K,uEAAYgL,qBAEnBC,cAAe,CACbC,aAAa,KAIJxL,EAAf,E,kBCdA8H,IAEA2D,IAASjJ,OAEP,kBAAC,WAAD,CAAUkJ,SAAU,kBAAC,EAAD,OAClB,kBAAC,GAAD,OAEF3I,SAAS4I,eAAe,SF6GpB,kBAAmBC,WACrBA,UAAUC,cAAcC,MAAMC,MAAK,SAAAC,GACjCA,EAAaC,kB","file":"static/js/main.1ea4089a.chunk.js","sourcesContent":["module.exports = __webpack_public_path__ + \"static/media/logo.91554ce9.svg\";","import React from 'react';\nimport { withTranslation } from 'react-i18next';\nimport './navBar.scss';\nimport logo from '../logo.svg';\n\nconst NavBar = ({ t }) => (\n <nav className=\"navbar navbar-expand-sm navbar-dark bg-primary \">\n <a className=\"navbar-brand\" href=\"/\">\n <img\n src={logo}\n width=\"30\"\n height=\"30\"\n className=\"d-inline-block align-text-bottom mr-2\"\n alt=\"\"\n />\n PiFi Radio\n </a>\n <button\n className=\"navbar-toggler\"\n type=\"button\"\n data-toggle=\"collapse\"\n data-target=\"#navbarNavAltMarkup\"\n aria-controls=\"navbarNavAltMarkup\"\n aria-expanded=\"false\"\n aria-label=\"Toggle navigation\"\n >\n <span className=\"navbar-toggler-icon\"></span>\n </button>\n <div className=\"collapse navbar-collapse\" id=\"navbarNavAltMarkup\">\n <div className=\"navbar-nav pt-1 ml-2\">\n {/* We use buttons and different class names instead of Bootstrap's\n anchor, so we don't get a warning for href=\"#\". */}\n <button\n className=\"btn btn-link nav-link\"\n data-toggle=\"modal\"\n data-target=\"#url-dialog\"\n >\n {t('playURL')}\n </button>\n\n <button\n className=\"btn btn-link nav-link\"\n data-toggle=\"modal\"\n data-target=\"#settings\"\n >\n {t('settings')}\n </button>\n </div>\n </div>\n </nav>\n);\nexport default withTranslation()(NavBar);\n","import React from 'react';\nimport { withTranslation } from 'react-i18next';\n\nconst PlayerStatus = ({ playerStatus, t }) => {\n const { playing, title } = playerStatus;\n\n return (\n <div className=\"text-center\">\n <h5 className=\"small text-uppercase\">\n {playing ? t('playing') : t('stopped')}\n </h5>\n <h3 className=\"ellipsis\">{title}</h3>\n </div>\n );\n};\n\nexport default withTranslation()(PlayerStatus);\n","import React from 'react';\nimport axios from 'axios';\nimport { toast } from 'react-toastify';\nimport { Translation } from 'react-i18next';\n\naxios.interceptors.response.use(null, error => {\n const expectedError =\n error.response &&\n error.response.status >= 400 &&\n error.response.status < 500;\n\n // We already alert about these with a backdrop on App.js\n const networkError = !error.response;\n\n // Ugly, but i18n only worked this way\n if (!expectedError && !networkError) {\n toast.error(\n <Translation>\n {(t, { i18n }) => <p>{t('errorUnexpected')}</p>}\n </Translation>\n );\n }\n // This expected error is universal in our app, so we'll place it here\n if (error.response && error.response.status === 403)\n toast.error(\n <Translation>{(t, { i18n }) => <p>{t('errorForbidden')}</p>}</Translation>\n );\n\n return Promise.reject(error);\n});\n\nexport default {\n get: axios.get,\n post: axios.post\n};\n","import http from './httpService.js';\n\nconst apiEndpoint = `${process.env.REACT_APP_API_URL}/player`;\n\nfunction body(method, params = null) {\n const body = new FormData();\n body.set('method', method);\n if (params) body.set('params', params);\n return body;\n}\n\nexport function getStatus() {\n return http.get(apiEndpoint);\n}\n\nexport function play() {\n return http.post(apiEndpoint, body('play'));\n}\n\nexport function stop() {\n return http.post(apiEndpoint, body('stop'));\n}\n\nexport function changeVol(delta) {\n return http.post(apiEndpoint, body('change_vol', delta));\n}\n\nexport function playRadio(name) {\n return http.post(apiEndpoint, body('play_radios', name));\n}\n\nexport function playURL(url) {\n return http.post(apiEndpoint, body('play_urls', url));\n}\n","import React from 'react';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faStop, faPlay } from '@fortawesome/free-solid-svg-icons';\nimport { play, stop } from '../services/playerService';\n\nconst PlayStopControl = ({ playing }) =>\n playing ? (\n <button className=\"btn btn-danger\" onClick={stop}>\n <FontAwesomeIcon icon={faStop} />\n </button>\n ) : (\n <button className=\"btn btn-dark\" onClick={play}>\n <FontAwesomeIcon icon={faPlay} />\n </button>\n );\n\nexport default PlayStopControl;\n","import React from 'react';\nimport PlayStopControl from './playStopControl';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faVolumeDown, faVolumeUp } from '@fortawesome/free-solid-svg-icons';\nimport { toast } from 'react-toastify';\nimport { withTranslation } from 'react-i18next';\nimport { changeVol } from '../services/playerService';\nimport { volTimeout } from '../config.json';\nimport './playerControls.scss';\n\nconst PlayerControls = ({ playerStatus, t }) => {\n const volDisabled = playerStatus.vol < 0;\n\n const handleVolChange = async delta => {\n const { data: vol } = await changeVol(delta);\n\n const toastId = 'vol';\n const toastMsg = `${t('volume')}: ${vol}%`;\n const toastOpts = { toastId, autoClose: volTimeout };\n if (toast.isActive(toastId)) toast.update(toastId, { render: toastMsg });\n else toast.info(toastMsg, toastOpts);\n };\n\n const renderVolButton = (delta, icon) => (\n <button\n className=\"btn btn-dark p-3\"\n disabled={volDisabled}\n onClick={() => handleVolChange(delta)}\n >\n <FontAwesomeIcon icon={icon} />\n </button>\n );\n\n return (\n <div className=\"player-controls btn-group w-100\">\n {renderVolButton('-5', faVolumeDown)}\n {renderVolButton('+5', faVolumeUp)}\n <PlayStopControl playing={playerStatus.playing} />\n </div>\n );\n};\n\nexport default withTranslation()(PlayerControls);\n","import React, { useEffect } from 'react';\nimport PlayerStatus from './playerStatus';\nimport PlayerControls from './playerControls';\nimport './player.scss';\n\nconst Player = ({ playerStatus }) => {\n useEffect(positionPlayerTop, [playerStatus]);\n\n return (\n <div className=\"player p-2\">\n <PlayerStatus playerStatus={playerStatus} />\n <PlayerControls playerStatus={playerStatus} />\n </div>\n );\n};\n\n// Calculate and set top for Player\n// Needed for desktop view with sticky\nconst positionPlayerTop = () => {\n const navHeight = document.querySelector('nav').clientHeight;\n const topMargin = 24;\n document.querySelector('.player').style.top = navHeight + topMargin + 'px';\n};\n\nexport default Player;\n","import React from 'react';\nimport { withTranslation } from 'react-i18next';\n\nconst SearchBox = ({ value, onChange, t }) => {\n return (\n <input\n className=\"form-control mb-4\"\n type=\"text\"\n id=\"query\"\n placeholder={t('search')}\n autoComplete=\"off\"\n value={value}\n onChange={e => onChange(e.target.value)}\n onFocus={() => onChange('')}\n ></input>\n );\n};\n\nexport default withTranslation()(SearchBox);\n","import React from 'react';\nimport './loader.scss';\n\nconst Loader = () => (\n <div className=\"loader\">\n <div className=\"spinner-border\" role=\"status\">\n <span className=\"sr-only\">Loading...</span>\n </div>\n </div>\n);\n\nexport default Loader;\n","import http from './httpService';\n\nconst apiEndpoint = `${process.env.REACT_APP_API_URL}/streams`;\n\nexport function getStreams() {\n return http.get(apiEndpoint);\n}\n","import React, { Component } from 'react';\nimport SearchBox from './common/searchBox';\nimport Loader from './loader';\nimport { withTranslation } from 'react-i18next';\nimport { toast } from 'react-toastify';\nimport { playRadio } from '../services/playerService';\nimport { getStreams } from '../services/streamsService';\nimport './streams.scss';\n\nclass Streams extends Component {\n state = { streams: {}, loading: true, query: '' };\n\n async componentDidMount() {\n const { data: streams } = await getStreams();\n this.setState({ streams, loading: false });\n }\n\n isPlaying = name => {\n return (\n name === this.props.playerStatus.title && this.props.playerStatus.playing\n );\n };\n\n handleItemClick = async name => {\n const { t, onBackdrop } = this.props;\n\n if (this.isPlaying(name)) return;\n\n onBackdrop(t('tunning'), name);\n try {\n await playRadio(name);\n } catch (ex) {\n if (ex.response && ex.response.status === 400)\n toast.error(t('errorNotFound'));\n }\n };\n\n getItemClasses = name => {\n const classes = 'streams__item ellipsis list-group-item ';\n return this.isPlaying(name)\n ? classes + 'active'\n : classes + 'list-group-item-action';\n };\n\n handleSearch = query => {\n this.setState({ query });\n };\n\n filteredStreams() {\n const { streams, query } = this.state;\n\n if (query === '') return streams;\n\n let filtered = {};\n for (let k in streams) {\n if (k.toLowerCase().includes(query.toLowerCase()) && !streams[k]) {\n filtered[k] = streams[k];\n }\n }\n return filtered;\n }\n\n renderList() {\n const streams = this.filteredStreams();\n\n if (Object.keys(streams).length === 0)\n return <h4 className=\"p-4\">{this.props.t('noStreams')}</h4>;\n\n return (\n <ul className=\"list-group list-group-flush\">\n {Object.keys(streams).map(name =>\n streams[name] ? (\n <li className=\"streams__header ellipsis\" key={name}>\n {name}\n </li>\n ) : (\n <li\n className={this.getItemClasses(name)}\n key={name}\n onClick={() => this.handleItemClick(name)}\n >\n {name}\n </li>\n )\n )}\n </ul>\n );\n }\n\n render() {\n if (this.state.loading) return <Loader />;\n\n return (\n <div className=\"streams p-2\">\n <SearchBox value={this.state.query} onChange={this.handleSearch} />\n {this.renderList()}\n </div>\n );\n }\n}\n\nexport default withTranslation()(Streams);\n","import React from 'react';\nimport PlayStopControl from './playStopControl';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faChevronUp } from '@fortawesome/free-solid-svg-icons';\nimport './miniPlayer.scss';\n\nconst MiniPlayer = ({ playerStatus }) => {\n const { title, playing } = playerStatus;\n return (\n <div className=\"mini-player p-3\">\n <div className=\"mini-player__left ellipsis pr-3\">\n <FontAwesomeIcon icon={faChevronUp} className=\"drawer__toggler mr-3\" />\n <span>{title}</span>\n </div>\n <PlayStopControl playing={playing} />\n </div>\n );\n};\n\nexport default MiniPlayer;\n","import React, { Component } from 'react';\nimport MiniPlayer from './miniPlayer';\nimport Player from './player';\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome';\nimport { faChevronDown } from '@fortawesome/free-solid-svg-icons';\nimport './drawer.scss';\n\nclass Drawer extends Component {\n state = { open: false };\n\n toggle = () => {\n this.setState({ open: !this.state.open });\n };\n\n handleClick = ({ target }) => {\n const blackList = target.tagName === 'BUTTON' || target.tagName === 'path';\n if (blackList) return;\n\n console.log('tag: ', target.tagName);\n console.log('class: ', target.className);\n this.toggle();\n };\n\n handleTouchStart = e => {\n this.setState({ touchStartY: e.touches[0].clientY });\n };\n\n handleTouchMove = e => {\n const { open, touchStartY } = this.state;\n const touchEndY = e.changedTouches[0].clientY;\n const touchUp = touchEndY < touchStartY;\n const touchDown = touchEndY > touchStartY;\n\n if (touchDown && open) this.toggle();\n else if (touchUp && !open) this.toggle();\n };\n\n render() {\n const { playerStatus } = this.props;\n let classes =\n 'drawer fixed-bottom bg-secondary border-top border-secondary';\n\n if (this.state.open) {\n classes += ' drawer--open';\n document.body.classList.add('body--drawer');\n } else document.body.classList.remove('body--drawer');\n\n return (\n <div\n className={classes}\n onClick={this.handleClick}\n onTouchStart={this.handleTouchStart}\n onTouchMove={this.handleTouchMove}\n >\n {this.state.open ? (\n <React.Fragment>\n <FontAwesomeIcon\n icon={faChevronDown}\n className=\"drawer__toggler fa-lg mb-3\"\n />\n <Player playerStatus={playerStatus} />{' '}\n </React.Fragment>\n ) : (\n <MiniPlayer playerStatus={playerStatus} />\n )}\n </div>\n );\n }\n}\n\nexport default Drawer;\n","import React from 'react';\n\nconst Modal = ({ id, title, footer, children }) => {\n return (\n <div\n className=\"modal fade\"\n id={id}\n tabIndex=\"-1\"\n role=\"dialog\"\n aria-labelledby=\"staticBackdropLabel\"\n aria-hidden=\"true\"\n >\n <div className=\"modal-dialog\" role=\"document\">\n <div className=\"modal-content\">\n <div className=\"modal-header\">\n <h5 className=\"modal-title\" id=\"staticBackdropLabel\">\n {title}\n </h5>\n <button\n type=\"button\"\n className=\"close\"\n data-dismiss=\"modal\"\n aria-label=\"Close\"\n >\n <span aria-hidden=\"true\">&times;</span>\n </button>\n </div>\n <div className=\"modal-body\">{children}</div>\n <div className=\"modal-footer\">{footer}</div>\n </div>\n </div>\n </div>\n );\n};\n\nexport default Modal;\n","const DEFAULT_THEME = 'darkly';\n\nconst STORAGE_KEY = 'theme';\n\nconst getThemePath = themeId =>\n `https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/${themeId}/bootstrap.min.css`;\n\nexport const themes = [\n { id: 'darkly', name: 'Darkly', themeColor: '#375a7f' },\n { id: 'lux', name: 'Lux', themeColor: '#1a1a1a' },\n { id: 'litera', name: 'Litera', themeColor: '#4582ec' }\n];\n\nexport function changeTheme(themeId) {\n localStorage.setItem(STORAGE_KEY, themeId);\n applyTheme();\n}\n\nexport function applyTheme() {\n const themeId = getThemeId();\n const themePath = getThemePath(themeId);\n const themeColor = themes.find(t => t.id === themeId).themeColor;\n\n const linkEl = document.querySelector('link[title=\"theme\"]');\n linkEl.setAttribute('href', themePath);\n\n const metaEl = document.querySelector('meta[name=\"theme-color\"]');\n metaEl.setAttribute('content', themeColor);\n}\n\nexport function getThemeId() {\n const localId = localStorage.getItem(STORAGE_KEY);\n\n if (localId === '') return DEFAULT_THEME;\n if (themes.filter(t => t.id === localId).length === 0) {\n localStorage.removeItem(STORAGE_KEY);\n return DEFAULT_THEME;\n }\n\n return localId;\n}\n","import React from 'react';\n\nconst Select = ({ id, label, row, data, ...rest }) => (\n <div className={row ? 'form-group row' : 'form-group'}>\n <label htmlFor={id} className=\"col-sm-2 col-form-label\">\n {label}\n </label>\n <div className=\"col-sm-10\">\n <select id={id} className=\"form-control\" {...rest}>\n {data.map(d => (\n <option key={d.id || d} value={d.id || d}>\n {d.name || d}\n </option>\n ))}\n </select>\n </div>\n </div>\n);\n\nexport default Select;\n","import React from 'react';\nimport Modal from './common/modal';\nimport { getThemeId, changeTheme, themes } from '../theme';\nimport i18next from 'i18next';\nimport { withTranslation } from 'react-i18next';\nimport { languages } from '../config.json';\nimport Select from './common/select';\n\nconst Settings = ({ t }) => {\n const renderFooter = () => (\n <button className=\"btn btn-secondary\" data-dismiss=\"modal\">\n {t('close')}\n </button>\n );\n\n return (\n <Modal id=\"settings\" title={t('settings')} footer={renderFooter()}>\n <Select\n id=\"theme-select\"\n label={t('theme')}\n data={themes}\n row\n value={getThemeId()}\n onChange={e => {\n changeTheme(e.target.value);\n }}\n />\n <Select\n id=\"language-select\"\n label={t('language')}\n data={languages}\n row\n value={i18next.language}\n onChange={e => {\n i18next.changeLanguage(e.target.value);\n }}\n />\n </Modal>\n );\n};\n\nexport default withTranslation()(Settings);\n","import React, { useState } from 'react';\nimport Modal from './common/modal';\nimport { toast } from 'react-toastify';\nimport { playURL } from '../services/playerService';\nimport { withTranslation } from 'react-i18next';\n\nconst URLDialog = ({ t }) => {\n const [url, setURL] = useState('');\n\n const handleOK = () => {\n if (url === '') return;\n doPlayURL(url);\n setURL('');\n };\n\n const doPlayURL = async url => {\n const errorToastOpts = {\n type: toast.TYPE.ERROR,\n render: t('errorNotFound')\n };\n\n const toastId = toast(t('tryingURL'));\n\n try {\n await playURL(url);\n } catch (ex) {\n if (ex.response && ex.response.status === 400)\n toast.update(toastId, errorToastOpts);\n }\n };\n\n const handleChange = ({ target: input }) => {\n setURL(input.value);\n };\n\n const handleKeyDown = e => {\n if (e.key === 'Enter') handleOK();\n };\n\n const renderFooter = () => (\n <React.Fragment>\n <button\n className=\"btn btn-primary\"\n data-dismiss=\"modal\"\n onClick={handleOK}\n >\n OK\n </button>\n <button\n className=\"btn btn-secondary\"\n onClick={() => setURL('')}\n data-dismiss=\"modal\"\n >\n Cancel\n </button>\n </React.Fragment>\n );\n\n return (\n <Modal id=\"url-dialog\" title={t('playURL')} footer={renderFooter()}>\n <input\n className=\"form-control mb-4\"\n type=\"text\"\n placeholder=\"URL\"\n value={url}\n onChange={handleChange}\n onKeyDown={handleKeyDown}\n />\n </Modal>\n );\n};\n\nexport default withTranslation()(URLDialog);\n","import React, { useState, useEffect } from 'react';\nimport './backdrop.scss';\n\nconst Backdrop = ({ title, body }) => {\n const [bodyStyled, setBodyStyled] = useState(false);\n\n const getScrollbarWidth = () =>\n window.innerWidth - document.documentElement.clientWidth;\n\n const doBodyStyle = () => {\n if (title) {\n // Calculate this BEFORE adding the class.\n document.body.style.paddingRight = getScrollbarWidth() + 'px';\n document.body.classList.add('body--backdrop');\n setBodyStyled(true);\n // We need this check because Bootstrap modals also style paddingRight\n } else if (!title && bodyStyled) {\n document.body.style.paddingRight = 0;\n document.body.classList.remove('body--backdrop');\n setBodyStyled(false);\n }\n };\n\n useEffect(doBodyStyle, [title, body]);\n\n const classes =\n 'backdrop p-2 text-white' + (title ? ' backdrop--visible' : '');\n\n // We need text-white on h* tags because some themes override it\n return (\n <div className={classes}>\n <h3 className=\"text-white\">{title}</h3>\n <h5 className=\"text-white\">{body}</h5>\n </div>\n );\n};\n\nexport default Backdrop;\n","import React, { Component } from 'react';\nimport NavBar from './components/navBar';\nimport Player from './components/player';\nimport Streams from './components/streams';\nimport Drawer from './components/drawer';\nimport Settings from './components/settings';\nimport URLDialog from './components/urlDialog';\nimport Loader from './components/loader';\nimport Backdrop from './components/backdrop';\nimport { ToastContainer } from 'react-toastify';\nimport { getStatus } from './services/playerService';\nimport { withTranslation } from 'react-i18next';\nimport { updateInterval, backdropTimeout } from './config.json';\nimport './App.scss';\nimport 'react-toastify/dist/ReactToastify.css';\n\nclass App extends Component {\n state = {\n playerStatus: {},\n loading: true,\n networkError: false,\n backdrop: {}\n };\n\n componentDidMount() {\n this.updatePlayerStatus();\n }\n\n async updatePlayerStatus() {\n try {\n const { data: playerStatus } = await getStatus();\n this.setState({ playerStatus, loading: false, networkError: false });\n } catch (ex) {\n this.setState({ loading: false, networkError: true });\n }\n\n setTimeout(() => this.updatePlayerStatus(), updateInterval);\n }\n\n handleBackdrop = (title, body = '') => {\n this.setState({ backdrop: { title, body } });\n setTimeout(() => this.setState({ backdrop: {} }), backdropTimeout);\n };\n\n render() {\n const { loading, networkError, backdrop, playerStatus } = this.state;\n const { t } = this.props;\n\n if (networkError) return <Backdrop title={t('errorNetwork')} />;\n if (loading) return <Loader />;\n if (!playerStatus.con_mpd) return <Backdrop title={t('disconnectedMPD')} />;\n\n return (\n <div className=\"app\">\n <ToastContainer />\n <Backdrop title={backdrop.title} body={backdrop.body} />\n <NavBar />\n <Drawer playerStatus={playerStatus} />\n\n <main className=\"app-main p-4\">\n <Player playerStatus={playerStatus} />\n <Streams\n onBackdrop={this.handleBackdrop}\n playerStatus={playerStatus}\n />\n <Settings />\n <URLDialog />\n </main>\n </div>\n );\n }\n}\n\nexport default withTranslation()(App);\n","// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on subsequent visits to a page, after all the\n// existing tabs open on the page have been closed, since previously cached\n// resources are updated in the background.\n\n// To learn more about the benefits of this model and instructions on how to\n// opt-in, read https://bit.ly/CRA-PWA\n\nconst isLocalhost = Boolean(\n window.location.hostname === 'localhost' ||\n // [::1] is the IPv6 localhost address.\n window.location.hostname === '[::1]' ||\n // 127.0.0.0/8 are considered localhost for IPv4.\n window.location.hostname.match(\n /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n )\n);\n\nexport function register(config) {\n if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n // The URL constructor is available in all browsers that support SW.\n const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);\n if (publicUrl.origin !== window.location.origin) {\n // Our service worker won't work if PUBLIC_URL is on a different origin\n // from what our page is served on. This might happen if a CDN is used to\n // serve assets; see https://github.com/facebook/create-react-app/issues/2374\n return;\n }\n\n window.addEventListener('load', () => {\n const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n if (isLocalhost) {\n // This is running on localhost. Let's check if a service worker still exists or not.\n checkValidServiceWorker(swUrl, config);\n\n // Add some additional logging to localhost, pointing developers to the\n // service worker/PWA documentation.\n navigator.serviceWorker.ready.then(() => {\n console.log(\n 'This web app is being served cache-first by a service ' +\n 'worker. To learn more, visit https://bit.ly/CRA-PWA'\n );\n });\n } else {\n // Is not localhost. Just register service worker\n registerValidSW(swUrl, config);\n }\n });\n }\n}\n\nfunction registerValidSW(swUrl, config) {\n navigator.serviceWorker\n .register(swUrl)\n .then(registration => {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing;\n if (installingWorker == null) {\n return;\n }\n installingWorker.onstatechange = () => {\n if (installingWorker.state === 'installed') {\n if (navigator.serviceWorker.controller) {\n // At this point, the updated precached content has been fetched,\n // but the previous service worker will still serve the older\n // content until all client tabs are closed.\n console.log(\n 'New content is available and will be used when all ' +\n 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'\n );\n\n // Execute callback\n if (config && config.onUpdate) {\n config.onUpdate(registration);\n }\n } else {\n // At this point, everything has been precached.\n // It's the perfect time to display a\n // \"Content is cached for offline use.\" message.\n console.log('Content is cached for offline use.');\n\n // Execute callback\n if (config && config.onSuccess) {\n config.onSuccess(registration);\n }\n }\n }\n };\n };\n })\n .catch(error => {\n console.error('Error during service worker registration:', error);\n });\n}\n\nfunction checkValidServiceWorker(swUrl, config) {\n // Check if the service worker can be found. If it can't reload the page.\n fetch(swUrl, {\n headers: { 'Service-Worker': 'script' }\n })\n .then(response => {\n // Ensure service worker exists, and that we really are getting a JS file.\n const contentType = response.headers.get('content-type');\n if (\n response.status === 404 ||\n (contentType != null && contentType.indexOf('javascript') === -1)\n ) {\n // No service worker found. Probably a different app. Reload the page.\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister().then(() => {\n window.location.reload();\n });\n });\n } else {\n // Service worker found. Proceed as normal.\n registerValidSW(swUrl, config);\n }\n })\n .catch(() => {\n console.log(\n 'No internet connection found. App is running in offline mode.'\n );\n });\n}\n\nexport function unregister() {\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.ready.then(registration => {\n registration.unregister();\n });\n }\n}\n","import i18n from 'i18next';\nimport { initReactI18next } from 'react-i18next';\n\nimport Backend from 'i18next-xhr-backend';\nimport LanguageDetector from 'i18next-browser-languagedetector';\n// not like to use this?\n// have a look at the Quick start guide\n// for passing in lng and translations on init\n\ni18n\n // load translation using xhr -> see /public/locales\n // learn more: https://github.com/i18next/i18next-xhr-backend\n .use(Backend)\n // detect user language\n // learn more: https://github.com/i18next/i18next-browser-languageDetector\n .use(LanguageDetector)\n // pass the i18n instance to react-i18next.\n .use(initReactI18next)\n // init i18next\n // for all options read: https://www.i18next.com/overview/configuration-options\n .init({\n fallbackLng: 'en',\n debug: process.env.REACT_APP_I18N_DEBUG,\n\n interpolation: {\n escapeValue: false // not needed for react as it escapes by default\n }\n });\n\nexport default i18n;\n","import React, { Suspense } from 'react';\nimport ReactDOM from 'react-dom';\nimport App from './App';\nimport * as serviceWorker from './serviceWorker';\nimport Loader from './components/loader';\nimport './i18n';\nimport { applyTheme } from './theme';\nimport './index.scss';\n\n// Bootstrap\n//import 'bootstrap/dist/css/bootstrap.css';\n//import 'bootswatch/dist/flatly/bootstrap.min.css'; // This replaces default Bootstrap file\nimport 'jquery/dist/jquery.min.js';\nimport 'bootstrap/dist/js/bootstrap.js';\n\napplyTheme();\n\nReactDOM.render(\n // Suspense needed for i18n\n <Suspense fallback={<Loader />}>\n <App />\n </Suspense>,\n document.getElementById('root')\n);\n\n// If you want your app to work offline and load faster, you can change\n// unregister() to register() below. Note this comes with some pitfalls.\n// Learn more about service workers: https://bit.ly/CRA-PWA\nserviceWorker.unregister();\n"],"sourceRoot":""}
@@ -1,91 +0,0 @@
1
- <div class="container">
2
-
3
- <!-- Player -->
4
- <div id="player" class="view">
5
-
6
- <div id="state">
7
- <h5 id="state-top-line">
8
- <span id="playing" class="text-uppercase">&nbsp;</span>
9
- <span id="progress">
10
- <span id="elapsed">&nbsp;</span>/<span id="length">&nbsp;</span>
11
- </span>
12
- </h5>
13
- <div id="progress-bar" class="bg-info"></div>
14
-
15
- <h3 id="title">&nbsp;</h3>
16
- <h4 id="artist">&nbsp;</h4>
17
- </div>
18
-
19
- <div id="controls" role="toolbar">
20
- <button type="button" id="btn-ps" class="btn btn-default btn-lg">
21
- <span id="span-ps" class="glyphicon"></span>
22
- </button>
23
- <button type="button" id="btn-vdown" class="btn btn-default btn-lg">
24
- <span class="glyphicon glyphicon-volume-down"></span>
25
- </button>
26
- <button type="button" id="btn-vup" class="btn btn-default btn-lg">
27
- <span class="glyphicon glyphicon-volume-up"></span>
28
- </button>
29
- </div>
30
-
31
-
32
- <div id="player-bottom">
33
- <% if play_local? %>
34
- <button type="button" id="btn-random" class="btn btn-info">
35
- <span class="glyphicon glyphicon-music"></span>
36
- <span id="lbl-random"></span>
37
- </button>
38
- <% end %>
39
- <button type="button" id="btn-radios" class="btn btn-primary" >
40
- <span class="glyphicon glyphicon-list"></span>
41
- <span id="lbl-radios"></span>
42
- </button>
43
- </div>
44
-
45
- <!-- Volume OSD -->
46
- <div id="osd">
47
- <h2 id="osd-text" class="text-info"></h2>
48
- </div>
49
-
50
- </div>
51
-
52
-
53
- <!-- Radios list -->
54
- <div id="radios" class="view">
55
-
56
- <h4 id="radios-welcome" class="text-center text-muted"></h4>
57
-
58
- <ul id="radios-list" class="list-group">
59
- <% streams_set.each do |key, value| %>
60
- <% if value.empty? %>
61
- <h4 class="radios-group-title"><%= key %></h4>
62
- <% else %>
63
- <a href="#">
64
- <li class="radio-name list-group-item"><h4><%= key %></h4></li>
65
- </a>
66
- <% end %>
67
- <% end %>
68
- </ul>
69
-
70
- <a href="#">
71
- <h4 id="insert" class="text-muted"></h4>
72
- </a>
73
-
74
- <div id="radios-bottom" class="text-center">
75
- <a href="#">
76
- <button type="button" id="btn-player" class="btn btn-primary">
77
- <span class="glyphicon glyphicon-th"></span>
78
- <span id="lbl-player"></span>
79
- </button>
80
- </a>
81
- </div>
82
- </div>
83
-
84
-
85
- <!-- To show alerts -->
86
- <div id="alert" class="view text-center text-info">
87
- <h3 id="alert-text-main"></h3>
88
- <h4 id="alert-text-more"></h4>
89
- </div>
90
-
91
- </div>
@@ -1,35 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <title><%= title %></title>
6
- <meta name="viewport" content="width=device-width, user-scalable=no">
7
-
8
- <!-- Icons (http://realfavicongenerator.net/) -->
9
- <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
10
- <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
11
- <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
12
- <link rel="manifest" href="/site.webmanifest">
13
- <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
14
- <meta name="apple-mobile-web-app-title" content="PiFi Radio">
15
- <meta name="application-name" content="PiFi Radio">
16
- <meta name="msapplication-TileColor" content="#da532c">
17
- <meta name="theme-color" content="#ffffff">
18
-
19
- <!-- Bootstrap CSS -->
20
- <link rel="stylesheet" href="/vendor/css/bootstrap.min.css">
21
- <!-- Optional Bootstrap theme -->
22
- <link rel="stylesheet" href="/vendor/css/bootstrap-theme.min.css">
23
- <!-- My stylesheet -->
24
- <link rel="stylesheet" href="/css/app.css">
25
- </head>
26
- <body>
27
- <%= yield %>
28
-
29
- <script src="/vendor/js/jquery.min.js"></script>
30
- <script src="/js/lang/<%= lang %>.js"></script>
31
- <script src="/js/app.js"></script>
32
- <script src="/vendor/js/bootstrap.min.js"></script>
33
- <noscript><h3 class="text-center">Enable JavaScript to use PiFi</h3></noscript>
34
- </body>
35
- </html>